Set up the basic structure and core functionality for the application
Initialize project with UI components, core libraries, and basic app structure. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 556aa286-edd2-4cea-8583-f4fc3cfd119b
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { Response } from 'express';
|
||||
import { aiService } from '../services/aiService';
|
||||
import { storage } from '../storage';
|
||||
import { AuthenticatedRequest } from '../middleware/auth';
|
||||
|
||||
export const aiController = {
|
||||
async getDailyJoke(req: any, res: Response) {
|
||||
try {
|
||||
// Check if we already have a joke for today
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const existingJokes = await storage.getAIInteractions(undefined, 'joke', 1);
|
||||
const todayJoke = existingJokes.find(joke => {
|
||||
const jokeDate = new Date(joke.createdAt);
|
||||
jokeDate.setHours(0, 0, 0, 0);
|
||||
return jokeDate.getTime() === today.getTime();
|
||||
});
|
||||
|
||||
if (todayJoke) {
|
||||
return res.json({
|
||||
joke: todayJoke.response,
|
||||
cached: true,
|
||||
timestamp: todayJoke.createdAt
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new daily joke
|
||||
const jokeResponse = await aiService.generateDailyJoke();
|
||||
|
||||
res.json({
|
||||
joke: jokeResponse.content,
|
||||
category: jokeResponse.category,
|
||||
cached: false,
|
||||
processingTime: jokeResponse.processingTime,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get daily joke error:', error);
|
||||
res.status(500).json({ error: 'Failed to get daily joke' });
|
||||
}
|
||||
},
|
||||
|
||||
async generateJoke(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { personalized } = req.query;
|
||||
|
||||
let jokeResponse;
|
||||
if (personalized === 'true') {
|
||||
jokeResponse = await aiService.generatePersonalizedJoke(req.user.id);
|
||||
} else {
|
||||
jokeResponse = await aiService.generateDailyJoke();
|
||||
}
|
||||
|
||||
res.json({
|
||||
joke: jokeResponse.content,
|
||||
category: jokeResponse.category,
|
||||
personalized: personalized === 'true',
|
||||
processingTime: jokeResponse.processingTime,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Generate joke error:', error);
|
||||
res.status(500).json({ error: 'Failed to generate joke' });
|
||||
}
|
||||
},
|
||||
|
||||
async chat(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { message } = req.body;
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
}
|
||||
|
||||
const response = await aiService.chat(req.user.id, message);
|
||||
|
||||
res.json({
|
||||
response: response.content,
|
||||
processingTime: response.processingTime,
|
||||
modelUsed: response.modelUsed,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('AI chat error:', error);
|
||||
res.status(500).json({ error: 'Failed to process chat message' });
|
||||
}
|
||||
},
|
||||
|
||||
async getInteractions(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { type, limit } = req.query;
|
||||
|
||||
const interactions = await storage.getAIInteractions(
|
||||
req.user.id,
|
||||
type as string,
|
||||
limit ? parseInt(limit as string) : undefined
|
||||
);
|
||||
|
||||
res.json({ interactions });
|
||||
} catch (error) {
|
||||
console.error('Get AI interactions error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch AI interactions' });
|
||||
}
|
||||
},
|
||||
|
||||
async getStatus(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const status = await aiService.getModelStatus();
|
||||
|
||||
res.json({
|
||||
...status,
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get AI status error:', error);
|
||||
res.status(500).json({ error: 'Failed to get AI status' });
|
||||
}
|
||||
},
|
||||
|
||||
async generateInsight(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { type } = req.body;
|
||||
|
||||
if (!type || !['financial', 'productivity', 'general'].includes(type)) {
|
||||
return res.status(400).json({ error: 'Valid insight type is required (financial, productivity, general)' });
|
||||
}
|
||||
|
||||
const insight = await aiService.generateInsight(req.user.id, type);
|
||||
|
||||
res.json({
|
||||
insight: insight.content,
|
||||
type,
|
||||
processingTime: insight.processingTime,
|
||||
modelUsed: insight.modelUsed,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Generate insight error:', error);
|
||||
res.status(500).json({ error: 'Failed to generate insight' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Request, Response } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { storage } from '../storage';
|
||||
import { loginSchema, registerSchema } from '@shared/schema';
|
||||
import { AuthenticatedRequest } from '../middleware/auth';
|
||||
|
||||
export const authController = {
|
||||
async register(req: Request, res: Response) {
|
||||
try {
|
||||
const validatedData = registerSchema.parse(req.body);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await storage.getUserByEmail(validatedData.email);
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'User already exists with this email' });
|
||||
}
|
||||
|
||||
const existingUsername = await storage.getUserByUsername(validatedData.username);
|
||||
if (existingUsername) {
|
||||
return res.status(400).json({ error: 'Username already taken' });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const saltRounds = 12;
|
||||
const hashedPassword = await bcrypt.hash(validatedData.password, saltRounds);
|
||||
|
||||
// Create user
|
||||
const user = await storage.createUser({
|
||||
username: validatedData.username,
|
||||
email: validatedData.email,
|
||||
password: hashedPassword,
|
||||
role: validatedData.role || 'standard',
|
||||
firstName: validatedData.firstName,
|
||||
lastName: validatedData.lastName,
|
||||
onboardingComplete: false,
|
||||
voiceEnabled: true,
|
||||
ttsEnabled: true
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
|
||||
res.status(201).json({
|
||||
message: 'User created successfully',
|
||||
user: userWithoutPassword
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
if (error instanceof Error && error.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'Invalid input data', details: error.message });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to create user' });
|
||||
}
|
||||
},
|
||||
|
||||
async login(req: Request, res: Response) {
|
||||
try {
|
||||
const validatedData = loginSchema.parse(req.body);
|
||||
|
||||
const user = await storage.getUserByEmail(validatedData.email);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid email or password' });
|
||||
}
|
||||
|
||||
const isValidPassword = await bcrypt.compare(validatedData.password, user.password);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ error: 'Invalid email or password' });
|
||||
}
|
||||
|
||||
// Use passport login
|
||||
req.login(user, (err) => {
|
||||
if (err) {
|
||||
console.error('Login error:', err);
|
||||
return res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
res.json({
|
||||
message: 'Login successful',
|
||||
user: userWithoutPassword
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
if (error instanceof Error && error.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'Invalid input data' });
|
||||
}
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
},
|
||||
|
||||
async logout(req: Request, res: Response) {
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
console.error('Logout error:', err);
|
||||
return res.status(500).json({ error: 'Logout failed' });
|
||||
}
|
||||
res.json({ message: 'Logout successful' });
|
||||
});
|
||||
},
|
||||
|
||||
async me(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { password, ...userWithoutPassword } = req.user;
|
||||
res.json({ user: userWithoutPassword });
|
||||
} catch (error) {
|
||||
console.error('Get user error:', error);
|
||||
res.status(500).json({ error: 'Failed to get user information' });
|
||||
}
|
||||
},
|
||||
|
||||
async updateProfile(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const allowedUpdates = ['firstName', 'lastName', 'voiceEnabled', 'ttsEnabled'];
|
||||
const updates: any = {};
|
||||
|
||||
for (const field of allowedUpdates) {
|
||||
if (req.body[field] !== undefined) {
|
||||
updates[field] = req.body[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return res.status(400).json({ error: 'No valid fields to update' });
|
||||
}
|
||||
|
||||
const updatedUser = await storage.updateUser(req.user.id, updates);
|
||||
if (!updatedUser) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const { password, ...userWithoutPassword } = updatedUser;
|
||||
res.json({
|
||||
message: 'Profile updated successfully',
|
||||
user: userWithoutPassword
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update profile error:', error);
|
||||
res.status(500).json({ error: 'Failed to update profile' });
|
||||
}
|
||||
},
|
||||
|
||||
async completeOnboarding(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { role, preferences } = req.body;
|
||||
|
||||
// Update user onboarding status and role if provided
|
||||
const updates: any = { onboardingComplete: true };
|
||||
if (role && ['standard', 'pro', 'admin'].includes(role)) {
|
||||
updates.role = role;
|
||||
}
|
||||
|
||||
const updatedUser = await storage.updateUser(req.user.id, updates);
|
||||
if (!updatedUser) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
// Save user preferences if provided
|
||||
if (preferences) {
|
||||
await storage.updateUserPreferences(req.user.id, preferences);
|
||||
}
|
||||
|
||||
const { password, ...userWithoutPassword } = updatedUser;
|
||||
res.json({
|
||||
message: 'Onboarding completed successfully',
|
||||
user: userWithoutPassword
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Complete onboarding error:', error);
|
||||
res.status(500).json({ error: 'Failed to complete onboarding' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
import { Response } from 'express';
|
||||
import { storage } from '../storage';
|
||||
import { financialRecordCreateSchema } from '@shared/schema';
|
||||
import { AuthenticatedRequest } from '../middleware/auth';
|
||||
|
||||
export const financialController = {
|
||||
async getRecords(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
let start: Date | undefined;
|
||||
let end: Date | undefined;
|
||||
|
||||
if (startDate) {
|
||||
start = new Date(startDate as string);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
end = new Date(endDate as string);
|
||||
}
|
||||
|
||||
const records = await storage.getFinancialRecords(req.user.id, start, end);
|
||||
|
||||
res.json({ records });
|
||||
} catch (error) {
|
||||
console.error('Get financial records error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch financial records' });
|
||||
}
|
||||
},
|
||||
|
||||
async getSummary(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { startDate, endDate, period } = req.query;
|
||||
|
||||
let start: Date | undefined;
|
||||
let end: Date | undefined;
|
||||
|
||||
if (period) {
|
||||
const now = new Date();
|
||||
switch (period) {
|
||||
case 'week':
|
||||
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
end = now;
|
||||
break;
|
||||
case 'month':
|
||||
start = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
break;
|
||||
case 'year':
|
||||
start = new Date(now.getFullYear(), 0, 1);
|
||||
end = new Date(now.getFullYear(), 11, 31);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (startDate) start = new Date(startDate as string);
|
||||
if (endDate) end = new Date(endDate as string);
|
||||
}
|
||||
|
||||
const summary = await storage.getFinancialSummary(req.user.id, start, end);
|
||||
|
||||
res.json({
|
||||
summary,
|
||||
period: period || 'custom',
|
||||
startDate: start?.toISOString(),
|
||||
endDate: end?.toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get financial summary error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch financial summary' });
|
||||
}
|
||||
},
|
||||
|
||||
async createRecord(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const validatedData = financialRecordCreateSchema.parse(req.body);
|
||||
|
||||
const record = await storage.createFinancialRecord({
|
||||
userId: req.user.id,
|
||||
type: validatedData.type,
|
||||
amount: validatedData.amount.toString(),
|
||||
category: validatedData.category,
|
||||
description: validatedData.description,
|
||||
date: new Date(validatedData.date),
|
||||
createdViaVoice: req.body.createdViaVoice || false,
|
||||
voiceTranscription: req.body.voiceTranscription,
|
||||
metadata: req.body.metadata
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Financial record created successfully',
|
||||
record
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create financial record error:', error);
|
||||
if (error instanceof Error && error.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'Invalid input data', details: error.message });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to create financial record' });
|
||||
}
|
||||
},
|
||||
|
||||
async updateRecord(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const recordId = parseInt(req.params.id);
|
||||
if (isNaN(recordId)) {
|
||||
return res.status(400).json({ error: 'Invalid record ID' });
|
||||
}
|
||||
|
||||
const allowedUpdates = ['type', 'amount', 'category', 'description', 'date', 'metadata'];
|
||||
const updates: any = {};
|
||||
|
||||
for (const field of allowedUpdates) {
|
||||
if (req.body[field] !== undefined) {
|
||||
if (field === 'date') {
|
||||
updates[field] = new Date(req.body[field]);
|
||||
} else if (field === 'amount') {
|
||||
updates[field] = req.body[field].toString();
|
||||
} else {
|
||||
updates[field] = req.body[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return res.status(400).json({ error: 'No valid fields to update' });
|
||||
}
|
||||
|
||||
const record = await storage.updateFinancialRecord(recordId, req.user.id, updates);
|
||||
if (!record) {
|
||||
return res.status(404).json({ error: 'Financial record not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: 'Financial record updated successfully',
|
||||
record
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update financial record error:', error);
|
||||
res.status(500).json({ error: 'Failed to update financial record' });
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRecord(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const recordId = parseInt(req.params.id);
|
||||
if (isNaN(recordId)) {
|
||||
return res.status(400).json({ error: 'Invalid record ID' });
|
||||
}
|
||||
|
||||
const deleted = await storage.deleteFinancialRecord(recordId, req.user.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: 'Financial record not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Financial record deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete financial record error:', error);
|
||||
res.status(500).json({ error: 'Failed to delete financial record' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Response } from 'express';
|
||||
import { storage } from '../storage';
|
||||
import { taskCreateSchema } from '@shared/schema';
|
||||
import { AuthenticatedRequest } from '../middleware/auth';
|
||||
|
||||
export const taskController = {
|
||||
async getTasks(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { status } = req.query;
|
||||
const tasks = await storage.getTasks(req.user.id, status as string);
|
||||
|
||||
res.json({ tasks });
|
||||
} catch (error) {
|
||||
console.error('Get tasks error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch tasks' });
|
||||
}
|
||||
},
|
||||
|
||||
async getTask(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const taskId = parseInt(req.params.id);
|
||||
if (isNaN(taskId)) {
|
||||
return res.status(400).json({ error: 'Invalid task ID' });
|
||||
}
|
||||
|
||||
const task = await storage.getTask(taskId, req.user.id);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Task not found' });
|
||||
}
|
||||
|
||||
res.json({ task });
|
||||
} catch (error) {
|
||||
console.error('Get task error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch task' });
|
||||
}
|
||||
},
|
||||
|
||||
async createTask(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const validatedData = taskCreateSchema.parse(req.body);
|
||||
|
||||
const task = await storage.createTask({
|
||||
userId: req.user.id,
|
||||
title: validatedData.title,
|
||||
description: validatedData.description,
|
||||
priority: validatedData.priority,
|
||||
dueDate: validatedData.dueDate ? new Date(validatedData.dueDate) : undefined,
|
||||
status: 'pending',
|
||||
createdViaVoice: req.body.createdViaVoice || false,
|
||||
voiceTranscription: req.body.voiceTranscription
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Task created successfully',
|
||||
task
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Create task error:', error);
|
||||
if (error instanceof Error && error.name === 'ZodError') {
|
||||
return res.status(400).json({ error: 'Invalid input data', details: error.message });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to create task' });
|
||||
}
|
||||
},
|
||||
|
||||
async updateTask(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const taskId = parseInt(req.params.id);
|
||||
if (isNaN(taskId)) {
|
||||
return res.status(400).json({ error: 'Invalid task ID' });
|
||||
}
|
||||
|
||||
const allowedUpdates = ['title', 'description', 'priority', 'status', 'dueDate'];
|
||||
const updates: any = {};
|
||||
|
||||
for (const field of allowedUpdates) {
|
||||
if (req.body[field] !== undefined) {
|
||||
if (field === 'dueDate' && req.body[field]) {
|
||||
updates[field] = new Date(req.body[field]);
|
||||
} else {
|
||||
updates[field] = req.body[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If marking as completed, set completedAt timestamp
|
||||
if (updates.status === 'completed') {
|
||||
updates.completedAt = new Date();
|
||||
} else if (updates.status && updates.status !== 'completed') {
|
||||
updates.completedAt = null;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return res.status(400).json({ error: 'No valid fields to update' });
|
||||
}
|
||||
|
||||
const task = await storage.updateTask(taskId, req.user.id, updates);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Task not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: 'Task updated successfully',
|
||||
task
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Update task error:', error);
|
||||
res.status(500).json({ error: 'Failed to update task' });
|
||||
}
|
||||
},
|
||||
|
||||
async deleteTask(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const taskId = parseInt(req.params.id);
|
||||
if (isNaN(taskId)) {
|
||||
return res.status(400).json({ error: 'Invalid task ID' });
|
||||
}
|
||||
|
||||
const deleted = await storage.deleteTask(taskId, req.user.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: 'Task not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Task deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete task error:', error);
|
||||
res.status(500).json({ error: 'Failed to delete task' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
import { Response } from 'express';
|
||||
import multer from 'multer';
|
||||
import { voiceService } from '../services/voiceService';
|
||||
import { storage } from '../storage';
|
||||
import { AuthenticatedRequest } from '../middleware/auth';
|
||||
|
||||
// Configure multer for audio file uploads
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024 // 10MB limit
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith('audio/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only audio files are allowed'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const voiceController = {
|
||||
async initialize(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const success = await voiceService.initialize();
|
||||
|
||||
if (success) {
|
||||
res.json({
|
||||
message: 'Voice models initialized successfully',
|
||||
status: 'ready'
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ error: 'Failed to initialize voice models' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Voice initialization error:', error);
|
||||
res.status(500).json({ error: 'Voice initialization failed' });
|
||||
}
|
||||
},
|
||||
|
||||
async processCommand(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { audioData } = req.body;
|
||||
|
||||
if (!audioData) {
|
||||
return res.status(400).json({ error: 'Audio data is required' });
|
||||
}
|
||||
|
||||
// Convert base64 audio to buffer
|
||||
const audioBuffer = Buffer.from(audioData, 'base64');
|
||||
|
||||
// Process audio to text
|
||||
const result = await voiceService.processAudioToText(audioBuffer, req.user.id);
|
||||
|
||||
// Execute the command based on intent
|
||||
let actionResult = null;
|
||||
if (result.intent && result.entities) {
|
||||
actionResult = await this.executeVoiceCommand(req.user.id, result.intent, result.entities);
|
||||
}
|
||||
|
||||
res.json({
|
||||
transcription: result.transcription,
|
||||
intent: result.intent,
|
||||
confidence: result.confidence,
|
||||
processingTime: result.processingTime,
|
||||
actionResult
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Voice command processing error:', error);
|
||||
res.status(500).json({ error: 'Failed to process voice command' });
|
||||
}
|
||||
},
|
||||
|
||||
async speak(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { text, voice } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: 'Text is required' });
|
||||
}
|
||||
|
||||
const result = await voiceService.textToSpeech(text, voice);
|
||||
|
||||
// Set appropriate headers for audio response
|
||||
res.set({
|
||||
'Content-Type': 'audio/wav',
|
||||
'Content-Length': result.audioBuffer.length
|
||||
});
|
||||
|
||||
res.send(result.audioBuffer);
|
||||
} catch (error) {
|
||||
console.error('Text-to-speech error:', error);
|
||||
res.status(500).json({ error: 'Failed to generate speech' });
|
||||
}
|
||||
},
|
||||
|
||||
async getCommands(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { limit } = req.query;
|
||||
const commands = await storage.getVoiceCommands(req.user.id, limit ? parseInt(limit as string) : undefined);
|
||||
|
||||
res.json({ commands });
|
||||
} catch (error) {
|
||||
console.error('Get voice commands error:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch voice commands' });
|
||||
}
|
||||
},
|
||||
|
||||
async getStatus(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const status = await voiceService.getModelStatus();
|
||||
|
||||
res.json({
|
||||
...status,
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get voice status error:', error);
|
||||
res.status(500).json({ error: 'Failed to get voice status' });
|
||||
}
|
||||
},
|
||||
|
||||
async executeVoiceCommand(userId: number, intent: string, entities: Record<string, any>) {
|
||||
try {
|
||||
switch (intent) {
|
||||
case 'task_create':
|
||||
if (entities.title) {
|
||||
const task = await storage.createTask({
|
||||
userId,
|
||||
title: entities.title,
|
||||
description: entities.description,
|
||||
priority: entities.priority || 'medium',
|
||||
dueDate: entities.dueDate ? new Date(entities.dueDate) : undefined,
|
||||
status: 'pending',
|
||||
createdViaVoice: true,
|
||||
voiceTranscription: entities.originalTranscription
|
||||
});
|
||||
return { type: 'task_created', task };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'expense_add':
|
||||
if (entities.amount && entities.category) {
|
||||
const record = await storage.createFinancialRecord({
|
||||
userId,
|
||||
type: 'expense',
|
||||
amount: entities.amount.toString(),
|
||||
category: entities.category,
|
||||
description: entities.description || 'Voice expense entry',
|
||||
date: new Date(),
|
||||
createdViaVoice: true,
|
||||
voiceTranscription: entities.originalTranscription
|
||||
});
|
||||
return { type: 'expense_added', record };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'income_add':
|
||||
if (entities.amount) {
|
||||
const record = await storage.createFinancialRecord({
|
||||
userId,
|
||||
type: 'income',
|
||||
amount: entities.amount.toString(),
|
||||
category: entities.category || 'income',
|
||||
description: entities.description || 'Voice income entry',
|
||||
date: new Date(),
|
||||
createdViaVoice: true,
|
||||
voiceTranscription: entities.originalTranscription
|
||||
});
|
||||
return { type: 'income_added', record };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_list':
|
||||
const tasks = await storage.getTasks(userId, 'pending');
|
||||
return { type: 'tasks_retrieved', tasks: tasks.slice(0, 5) }; // Limit for voice response
|
||||
|
||||
case 'financial_summary':
|
||||
const summary = await storage.getFinancialSummary(userId);
|
||||
return { type: 'financial_summary', summary };
|
||||
|
||||
default:
|
||||
return { type: 'unknown_intent', message: 'I didn\'t understand that command' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Execute voice command error:', error);
|
||||
return { type: 'error', message: 'Failed to execute command' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Pool, neonConfig } from '@neondatabase/serverless';
|
||||
import { drizzle } from 'drizzle-orm/neon-serverless';
|
||||
import ws from "ws";
|
||||
import * as schema from "@shared/schema";
|
||||
|
||||
neonConfig.webSocketConstructor = ws;
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error(
|
||||
"DATABASE_URL must be set. Did you forget to provision a database?",
|
||||
);
|
||||
}
|
||||
|
||||
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
export const db = drizzle({ client: pool, schema });
|
||||
@@ -0,0 +1,70 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
||||
|
||||
const originalResJson = res.json;
|
||||
res.json = function (bodyJson, ...args) {
|
||||
capturedJsonResponse = bodyJson;
|
||||
return originalResJson.apply(res, [bodyJson, ...args]);
|
||||
};
|
||||
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
if (path.startsWith("/api")) {
|
||||
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
||||
if (capturedJsonResponse) {
|
||||
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
||||
}
|
||||
|
||||
if (logLine.length > 80) {
|
||||
logLine = logLine.slice(0, 79) + "…";
|
||||
}
|
||||
|
||||
log(logLine);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const server = await registerRoutes(app);
|
||||
|
||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||
const status = err.status || err.statusCode || 500;
|
||||
const message = err.message || "Internal Server Error";
|
||||
|
||||
res.status(status).json({ message });
|
||||
throw err;
|
||||
});
|
||||
|
||||
// importantly only setup vite in development and after
|
||||
// setting up all the other routes so the catch-all route
|
||||
// doesn't interfere with the other routes
|
||||
if (app.get("env") === "development") {
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
// ALWAYS serve the app on port 5000
|
||||
// this serves both the API and the client.
|
||||
// It is the only port that is not firewalled.
|
||||
const port = 5000;
|
||||
server.listen({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
reusePort: true,
|
||||
}, () => {
|
||||
log(`serving on port ${port}`);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { storage } from '../storage';
|
||||
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user?: any;
|
||||
}
|
||||
|
||||
export const authMiddleware = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Refresh user data from database
|
||||
const user = await storage.getUser(req.user.id);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Auth middleware error:', error);
|
||||
res.status(500).json({ error: 'Authentication error' });
|
||||
}
|
||||
};
|
||||
|
||||
export const requireRole = (roles: string[]) => {
|
||||
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
if (!roles.includes(req.user.role)) {
|
||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
export const requireOnboarding = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
if (!req.user.onboardingComplete) {
|
||||
return res.status(403).json({ error: 'Onboarding required', onboardingRequired: true });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import express from "express";
|
||||
import session from "express-session";
|
||||
import passport from "passport";
|
||||
import { Strategy as LocalStrategy } from "passport-local";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { storage } from "./storage";
|
||||
import { authMiddleware, requireRole } from "./middleware/auth";
|
||||
import { authController } from "./controllers/authController";
|
||||
import { taskController } from "./controllers/taskController";
|
||||
import { financialController } from "./controllers/financialController";
|
||||
import { voiceController } from "./controllers/voiceController";
|
||||
import { aiController } from "./controllers/aiController";
|
||||
|
||||
// Configure passport
|
||||
passport.use(new LocalStrategy(
|
||||
{ usernameField: 'email' },
|
||||
async (email, password, done) => {
|
||||
try {
|
||||
const user = await storage.getUserByEmail(email);
|
||||
if (!user) {
|
||||
return done(null, false, { message: 'User not found' });
|
||||
}
|
||||
|
||||
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||
if (!isValidPassword) {
|
||||
return done(null, false, { message: 'Invalid password' });
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
} catch (error) {
|
||||
return done(error);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
passport.serializeUser((user: any, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser(async (id: number, done) => {
|
||||
try {
|
||||
const user = await storage.getUser(id);
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Configure session middleware
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// Authentication routes
|
||||
app.post('/api/auth/register', authController.register);
|
||||
app.post('/api/auth/login', authController.login);
|
||||
app.post('/api/auth/logout', authController.logout);
|
||||
app.get('/api/auth/me', authMiddleware, authController.me);
|
||||
app.put('/api/auth/profile', authMiddleware, authController.updateProfile);
|
||||
app.post('/api/auth/complete-onboarding', authMiddleware, authController.completeOnboarding);
|
||||
|
||||
// Task routes
|
||||
app.get('/api/tasks', authMiddleware, taskController.getTasks);
|
||||
app.get('/api/tasks/:id', authMiddleware, taskController.getTask);
|
||||
app.post('/api/tasks', authMiddleware, taskController.createTask);
|
||||
app.put('/api/tasks/:id', authMiddleware, taskController.updateTask);
|
||||
app.delete('/api/tasks/:id', authMiddleware, taskController.deleteTask);
|
||||
|
||||
// Financial routes
|
||||
app.get('/api/financial/records', authMiddleware, financialController.getRecords);
|
||||
app.get('/api/financial/summary', authMiddleware, financialController.getSummary);
|
||||
app.post('/api/financial/records', authMiddleware, financialController.createRecord);
|
||||
app.put('/api/financial/records/:id', authMiddleware, financialController.updateRecord);
|
||||
app.delete('/api/financial/records/:id', authMiddleware, financialController.deleteRecord);
|
||||
|
||||
// Voice processing routes
|
||||
app.post('/api/voice/initialize', authMiddleware, voiceController.initialize);
|
||||
app.post('/api/voice/process-command', authMiddleware, voiceController.processCommand);
|
||||
app.post('/api/voice/speak', authMiddleware, voiceController.speak);
|
||||
app.get('/api/voice/commands', authMiddleware, voiceController.getCommands);
|
||||
app.get('/api/voice/status', authMiddleware, voiceController.getStatus);
|
||||
|
||||
// AI routes
|
||||
app.get('/api/ai/daily-joke', aiController.getDailyJoke);
|
||||
app.post('/api/ai/generate-joke', authMiddleware, aiController.generateJoke);
|
||||
app.post('/api/ai/chat', authMiddleware, aiController.chat);
|
||||
app.get('/api/ai/interactions', authMiddleware, aiController.getInteractions);
|
||||
app.get('/api/ai/status', authMiddleware, aiController.getStatus);
|
||||
|
||||
// Admin routes (dev/admin role only)
|
||||
app.get('/api/admin/users', authMiddleware, requireRole(['admin']), async (req, res) => {
|
||||
// Admin functionality - would implement user management
|
||||
res.json({ message: 'Admin endpoint - user management would be implemented here' });
|
||||
});
|
||||
|
||||
app.get('/api/admin/system-status', authMiddleware, requireRole(['admin']), async (req, res) => {
|
||||
try {
|
||||
// System status check
|
||||
const status = {
|
||||
database: 'connected',
|
||||
voiceModels: 'loaded',
|
||||
aiModels: 'loaded',
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to get system status' });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
});
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
return httpServer;
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
import { storage } from '../storage';
|
||||
|
||||
export interface AIResponse {
|
||||
content: string;
|
||||
processingTime: number;
|
||||
modelUsed: string;
|
||||
}
|
||||
|
||||
export interface JokeResponse extends AIResponse {
|
||||
category: string;
|
||||
}
|
||||
|
||||
class AIService {
|
||||
private modelPath: string;
|
||||
private isInitialized: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.modelPath = process.env.AI_MODEL_PATH || './models/ai/base-llm';
|
||||
}
|
||||
|
||||
async initialize(): Promise<boolean> {
|
||||
try {
|
||||
console.log('Initializing AI models...');
|
||||
|
||||
// In production, this would load the actual local LLM
|
||||
await this.loadModel();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('AI models initialized successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize AI models:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadModel(): Promise<void> {
|
||||
// Simulate model loading time
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
console.log(`AI model loaded from ${this.modelPath}`);
|
||||
}
|
||||
|
||||
async generateDailyJoke(): Promise<JokeResponse> {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// In production, this would use the actual AI model
|
||||
const joke = await this.simulateJokeGeneration();
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
const response: JokeResponse = {
|
||||
content: joke,
|
||||
processingTime,
|
||||
modelUsed: 'local-llm-base',
|
||||
category: 'programming'
|
||||
};
|
||||
|
||||
// Store the interaction
|
||||
await storage.createAIInteraction({
|
||||
userId: null, // Daily joke is global
|
||||
type: 'joke',
|
||||
prompt: 'Generate daily programming joke',
|
||||
response: joke,
|
||||
modelUsed: 'local-llm-base',
|
||||
processingTime,
|
||||
wasSpoken: false
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('AI joke generation error:', error);
|
||||
throw new Error('Failed to generate joke');
|
||||
}
|
||||
}
|
||||
|
||||
async generatePersonalizedJoke(userId: number, preferences?: any): Promise<JokeResponse> {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Get user context for personalization
|
||||
const userTasks = await storage.getTasks(userId);
|
||||
const recentCommands = await storage.getVoiceCommands(userId, 5);
|
||||
|
||||
const joke = await this.simulatePersonalizedJoke(userTasks.length, recentCommands.length);
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
const response: JokeResponse = {
|
||||
content: joke,
|
||||
processingTime,
|
||||
modelUsed: 'local-llm-base',
|
||||
category: 'personalized'
|
||||
};
|
||||
|
||||
// Store the interaction
|
||||
await storage.createAIInteraction({
|
||||
userId,
|
||||
type: 'joke',
|
||||
prompt: 'Generate personalized joke',
|
||||
response: joke,
|
||||
modelUsed: 'local-llm-base',
|
||||
processingTime,
|
||||
wasSpoken: false
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Personalized joke generation error:', error);
|
||||
throw new Error('Failed to generate personalized joke');
|
||||
}
|
||||
}
|
||||
|
||||
async chat(userId: number, message: string): Promise<AIResponse> {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Get user context
|
||||
const user = await storage.getUser(userId);
|
||||
const recentTasks = await storage.getTasks(userId);
|
||||
const financialSummary = await storage.getFinancialSummary(userId);
|
||||
|
||||
const response = await this.simulateChat(message, {
|
||||
user,
|
||||
taskCount: recentTasks.length,
|
||||
financialSummary
|
||||
});
|
||||
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
const aiResponse: AIResponse = {
|
||||
content: response,
|
||||
processingTime,
|
||||
modelUsed: 'local-llm-base'
|
||||
};
|
||||
|
||||
// Store the interaction
|
||||
await storage.createAIInteraction({
|
||||
userId,
|
||||
type: 'response',
|
||||
prompt: message,
|
||||
response,
|
||||
modelUsed: 'local-llm-base',
|
||||
processingTime,
|
||||
wasSpoken: false
|
||||
});
|
||||
|
||||
return aiResponse;
|
||||
} catch (error) {
|
||||
console.error('AI chat error:', error);
|
||||
throw new Error('Failed to process chat message');
|
||||
}
|
||||
}
|
||||
|
||||
async generateInsight(userId: number, type: 'financial' | 'productivity' | 'general'): Promise<AIResponse> {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
let insight = '';
|
||||
|
||||
switch (type) {
|
||||
case 'financial':
|
||||
const summary = await storage.getFinancialSummary(userId);
|
||||
insight = await this.simulateFinancialInsight(summary);
|
||||
break;
|
||||
case 'productivity':
|
||||
const tasks = await storage.getTasks(userId);
|
||||
insight = await this.simulateProductivityInsight(tasks);
|
||||
break;
|
||||
default:
|
||||
insight = await this.simulateGeneralInsight();
|
||||
}
|
||||
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
const response: AIResponse = {
|
||||
content: insight,
|
||||
processingTime,
|
||||
modelUsed: 'local-llm-base'
|
||||
};
|
||||
|
||||
// Store the interaction
|
||||
await storage.createAIInteraction({
|
||||
userId,
|
||||
type: 'insight',
|
||||
prompt: `Generate ${type} insight`,
|
||||
response: insight,
|
||||
modelUsed: 'local-llm-base',
|
||||
processingTime,
|
||||
wasSpoken: false
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('AI insight generation error:', error);
|
||||
throw new Error('Failed to generate insight');
|
||||
}
|
||||
}
|
||||
|
||||
private async simulateJokeGeneration(): Promise<string> {
|
||||
const jokes = [
|
||||
"Why don't developers ever finish their tasks? Because they always get stuck in an infinite loop of 'just one more feature'!",
|
||||
"Why did the programmer quit his job? He didn't get arrays! (a raise)",
|
||||
"How many programmers does it take to screw in a light bulb? None, that's a hardware problem!",
|
||||
"Why do Java developers wear glasses? Because they can't C#!",
|
||||
"A SQL query walks into a bar, approaches two tables and asks: 'Can I join you?'",
|
||||
"Why don't tasks ever get lonely? Because they always have deadlines to keep them company!",
|
||||
"What's a programmer's favorite hangout place? The Foo Bar!",
|
||||
"Why did the developer go broke? Because he used up all his cache!"
|
||||
];
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
return jokes[Math.floor(Math.random() * jokes.length)];
|
||||
}
|
||||
|
||||
private async simulatePersonalizedJoke(taskCount: number, voiceCommandCount: number): Promise<string> {
|
||||
let joke = '';
|
||||
|
||||
if (taskCount > 10) {
|
||||
joke = "Looks like you're collecting tasks like Pokémon cards! Gotta catch 'em all... and then actually do them!";
|
||||
} else if (taskCount === 0) {
|
||||
joke = "Your task list is emptier than a JavaScript developer's knowledge of semicolons!";
|
||||
} else if (voiceCommandCount > 5) {
|
||||
joke = "You're talking to me more than most people talk to their houseplants. At least I talk back!";
|
||||
} else {
|
||||
joke = await this.simulateJokeGeneration();
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return joke;
|
||||
}
|
||||
|
||||
private async simulateChat(message: string, context: any): Promise<string> {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
if (lowerMessage.includes('task') || lowerMessage.includes('todo')) {
|
||||
if (context.taskCount === 0) {
|
||||
return "You have no pending tasks right now. Great job staying on top of things! Ready to add something new?";
|
||||
} else {
|
||||
return `You currently have ${context.taskCount} pending tasks. Would you like me to help you prioritize them or create a new one?`;
|
||||
}
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('money') || lowerMessage.includes('financial') || lowerMessage.includes('budget')) {
|
||||
const { income, expenses, net } = context.financialSummary;
|
||||
return `Your current financial status shows $${income} in income and $${expenses} in expenses, for a net of $${net}. ${net > 0 ? "You're in the positive!" : "Consider reviewing your expenses."}`;
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('help')) {
|
||||
return "I can help you with tasks, financial tracking, and answering questions about your data. Try saying 'create task', 'add expense', or ask about your financial summary!";
|
||||
}
|
||||
|
||||
if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {
|
||||
return `Hello ${context.user?.firstName || 'there'}! I'm your AI assistant. How can I help you manage your tasks and finances today?`;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
return "I understand you're asking about that. While I'm still learning, I can help you with tasks, expenses, and general productivity questions. Is there something specific I can assist with?";
|
||||
}
|
||||
|
||||
private async simulateFinancialInsight(summary: { income: number; expenses: number; net: number }): Promise<string> {
|
||||
await new Promise(resolve => setTimeout(resolve, 1200));
|
||||
|
||||
const { income, expenses, net } = summary;
|
||||
const expenseRatio = income > 0 ? (expenses / income) * 100 : 0;
|
||||
|
||||
if (net > 0) {
|
||||
if (expenseRatio < 70) {
|
||||
return `Excellent financial management! You're spending only ${expenseRatio.toFixed(1)}% of your income. Consider investing the surplus for long-term growth.`;
|
||||
} else {
|
||||
return `Good job maintaining a positive balance! Your expense ratio is ${expenseRatio.toFixed(1)}%. Consider optimizing some categories to improve savings.`;
|
||||
}
|
||||
} else {
|
||||
return `Your expenses exceed income by $${Math.abs(net)}. I'd recommend reviewing your spending categories and identifying areas where you can reduce costs.`;
|
||||
}
|
||||
}
|
||||
|
||||
private async simulateProductivityInsight(tasks: any[]): Promise<string> {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const completedTasks = tasks.filter(t => t.status === 'completed');
|
||||
const pendingTasks = tasks.filter(t => t.status === 'pending');
|
||||
const overdueTasks = tasks.filter(t => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== 'completed');
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return "Your task list is clear! This is a great time to plan ahead or tackle some long-term goals.";
|
||||
}
|
||||
|
||||
if (overdueTasks.length > 0) {
|
||||
return `You have ${overdueTasks.length} overdue tasks. Consider prioritizing these first to get back on track.`;
|
||||
}
|
||||
|
||||
if (completedTasks.length > pendingTasks.length) {
|
||||
return `Great productivity! You've completed ${completedTasks.length} tasks. Keep up the momentum with your remaining ${pendingTasks.length} tasks.`;
|
||||
}
|
||||
|
||||
return `You have ${pendingTasks.length} pending tasks. Consider breaking larger tasks into smaller, manageable chunks for better progress.`;
|
||||
}
|
||||
|
||||
private async simulateGeneralInsight(): Promise<string> {
|
||||
const insights = [
|
||||
"Remember: Progress, not perfection. Small daily improvements lead to remarkable long-term results.",
|
||||
"Voice commands can speed up your workflow by 40%. Try using them for quick task creation and expense logging.",
|
||||
"The best time to plan tomorrow is today. Spend 5 minutes each evening reviewing and organizing.",
|
||||
"Your data stays completely private with local processing. No cloud, no worries!",
|
||||
"Consistency beats intensity. Better to track expenses daily than to do it all at month-end."
|
||||
];
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
return insights[Math.floor(Math.random() * insights.length)];
|
||||
}
|
||||
|
||||
async getModelStatus(): Promise<{ loaded: boolean; modelPath: string; initialized: boolean }> {
|
||||
return {
|
||||
loaded: this.isInitialized,
|
||||
modelPath: this.modelPath,
|
||||
initialized: this.isInitialized
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const aiService = new AIService();
|
||||
@@ -0,0 +1,330 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { storage } from '../storage';
|
||||
|
||||
export interface VoiceProcessingResult {
|
||||
transcription: string;
|
||||
confidence: number;
|
||||
processingTime: number;
|
||||
intent?: string;
|
||||
entities?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface TTSResult {
|
||||
audioBuffer: Buffer;
|
||||
processingTime: number;
|
||||
}
|
||||
|
||||
class VoiceService {
|
||||
private sttModelPath: string;
|
||||
private ttsModelPath: string;
|
||||
private isInitialized: boolean = false;
|
||||
private sttProcess: ChildProcess | null = null;
|
||||
private ttsProcess: ChildProcess | null = null;
|
||||
|
||||
constructor() {
|
||||
this.sttModelPath = process.env.STT_MODEL_PATH || './models/speech/base-stt';
|
||||
this.ttsModelPath = process.env.TTS_MODEL_PATH || './models/speech/base-tts';
|
||||
}
|
||||
|
||||
async initialize(): Promise<boolean> {
|
||||
try {
|
||||
console.log('Initializing voice models...');
|
||||
|
||||
// Check if model files exist
|
||||
const sttExists = await this.checkModelExists(this.sttModelPath);
|
||||
const ttsExists = await this.checkModelExists(this.ttsModelPath);
|
||||
|
||||
if (!sttExists || !ttsExists) {
|
||||
console.error('Voice models not found. Please run model setup script.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize STT model (using whisper.cpp or similar)
|
||||
await this.initializeSTT();
|
||||
|
||||
// Initialize TTS model (using espeak-ng or similar)
|
||||
await this.initializeTTS();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('Voice models initialized successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize voice models:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async checkModelExists(modelPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(modelPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeSTT(): Promise<void> {
|
||||
// Initialize STT process - in production, this would start the actual model
|
||||
console.log(`Loading STT model from ${this.sttModelPath}`);
|
||||
// For now, we'll simulate model loading
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
private async initializeTTS(): Promise<void> {
|
||||
// Initialize TTS process - in production, this would start the actual model
|
||||
console.log(`Loading TTS model from ${this.ttsModelPath}`);
|
||||
// For now, we'll simulate model loading
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
async processAudioToText(audioBuffer: Buffer, userId?: number): Promise<VoiceProcessingResult> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Voice service not initialized');
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// In production, this would use actual STT processing
|
||||
// For now, we'll simulate transcription
|
||||
const transcription = await this.simulateSTT(audioBuffer);
|
||||
const confidence = 0.95; // Simulated confidence
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
// Extract intent and entities from transcription
|
||||
const { intent, entities } = this.extractIntent(transcription);
|
||||
|
||||
const result: VoiceProcessingResult = {
|
||||
transcription,
|
||||
confidence,
|
||||
processingTime,
|
||||
intent,
|
||||
entities
|
||||
};
|
||||
|
||||
// Log voice command if user is provided
|
||||
if (userId) {
|
||||
await storage.createVoiceCommand({
|
||||
userId,
|
||||
command: transcription,
|
||||
transcription,
|
||||
intent,
|
||||
confidence,
|
||||
processingTime,
|
||||
successful: true
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
if (userId) {
|
||||
await storage.createVoiceCommand({
|
||||
userId,
|
||||
command: '',
|
||||
transcription: '',
|
||||
confidence: 0,
|
||||
processingTime,
|
||||
successful: false,
|
||||
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async textToSpeech(text: string, voice?: string): Promise<TTSResult> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Voice service not initialized');
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// In production, this would use actual TTS processing
|
||||
const audioBuffer = await this.simulateTTS(text, voice);
|
||||
const processingTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
audioBuffer,
|
||||
processingTime
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('TTS error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async simulateSTT(audioBuffer: Buffer): Promise<string> {
|
||||
// Simulate STT processing delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Return a simulated transcription based on common commands
|
||||
const sampleCommands = [
|
||||
"Create task review budget reports",
|
||||
"Add expense coffee shop four dollars fifty cents",
|
||||
"Show me today's tasks",
|
||||
"What's my financial summary",
|
||||
"Schedule meeting with team for Friday",
|
||||
"Mark task as completed",
|
||||
"Add income freelance payment five hundred dollars"
|
||||
];
|
||||
|
||||
return sampleCommands[Math.floor(Math.random() * sampleCommands.length)];
|
||||
}
|
||||
|
||||
private async simulateTTS(text: string, voice?: string): Promise<Buffer> {
|
||||
// Simulate TTS processing delay
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Return a dummy audio buffer (in production, this would be actual audio)
|
||||
const dummyAudioData = Buffer.alloc(1024, 0);
|
||||
return dummyAudioData;
|
||||
}
|
||||
|
||||
private extractIntent(transcription: string): { intent?: string; entities?: Record<string, any> } {
|
||||
const text = transcription.toLowerCase();
|
||||
|
||||
// Simple intent recognition based on keywords
|
||||
if (text.includes('create') && (text.includes('task') || text.includes('todo'))) {
|
||||
return {
|
||||
intent: 'task_create',
|
||||
entities: this.extractTaskEntities(text)
|
||||
};
|
||||
}
|
||||
|
||||
if (text.includes('add') && (text.includes('expense') || text.includes('cost'))) {
|
||||
return {
|
||||
intent: 'expense_add',
|
||||
entities: this.extractExpenseEntities(text)
|
||||
};
|
||||
}
|
||||
|
||||
if (text.includes('add') && text.includes('income')) {
|
||||
return {
|
||||
intent: 'income_add',
|
||||
entities: this.extractIncomeEntities(text)
|
||||
};
|
||||
}
|
||||
|
||||
if (text.includes('show') || text.includes('list') || text.includes('get')) {
|
||||
if (text.includes('task')) {
|
||||
return { intent: 'task_list' };
|
||||
}
|
||||
if (text.includes('financial') || text.includes('money') || text.includes('summary')) {
|
||||
return { intent: 'financial_summary' };
|
||||
}
|
||||
}
|
||||
|
||||
if (text.includes('complete') || text.includes('done') || text.includes('finish')) {
|
||||
return { intent: 'task_complete' };
|
||||
}
|
||||
|
||||
return { intent: 'unknown' };
|
||||
}
|
||||
|
||||
private extractTaskEntities(text: string): Record<string, any> {
|
||||
const entities: Record<string, any> = {};
|
||||
|
||||
// Extract task title (everything after "create task" or similar)
|
||||
const taskMatch = text.match(/(?:create|add|new)\s+(?:task|todo)\s+(.+)/i);
|
||||
if (taskMatch) {
|
||||
entities.title = taskMatch[1].trim();
|
||||
}
|
||||
|
||||
// Extract priority
|
||||
if (text.includes('high priority') || text.includes('urgent')) {
|
||||
entities.priority = 'high';
|
||||
} else if (text.includes('low priority')) {
|
||||
entities.priority = 'low';
|
||||
} else {
|
||||
entities.priority = 'medium';
|
||||
}
|
||||
|
||||
// Extract due date (simplified)
|
||||
if (text.includes('today')) {
|
||||
entities.dueDate = new Date().toISOString();
|
||||
} else if (text.includes('tomorrow')) {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
entities.dueDate = tomorrow.toISOString();
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
private extractExpenseEntities(text: string): Record<string, any> {
|
||||
const entities: Record<string, any> = {};
|
||||
|
||||
// Extract amount
|
||||
const amountMatch = text.match(/(?:\$|dollar|dollars?)\s*(\d+(?:\.\d{2})?)|(\d+(?:\.\d{2})?)\s*(?:dollar|dollars?)/i);
|
||||
if (amountMatch) {
|
||||
entities.amount = parseFloat(amountMatch[1] || amountMatch[2]);
|
||||
}
|
||||
|
||||
// Extract category/description
|
||||
const categories = ['food', 'transport', 'shopping', 'utilities', 'entertainment', 'healthcare'];
|
||||
for (const category of categories) {
|
||||
if (text.includes(category)) {
|
||||
entities.category = category;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entities.category) {
|
||||
entities.category = 'other';
|
||||
}
|
||||
|
||||
// Extract description
|
||||
const expenseMatch = text.match(/(?:expense|spent|paid)\s+(?:for\s+)?(.+?)(?:\s+\$|\s+\d+|$)/i);
|
||||
if (expenseMatch) {
|
||||
entities.description = expenseMatch[1].trim();
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
private extractIncomeEntities(text: string): Record<string, any> {
|
||||
const entities: Record<string, any> = {};
|
||||
|
||||
// Extract amount
|
||||
const amountMatch = text.match(/(?:\$|dollar|dollars?)\s*(\d+(?:\.\d{2})?)|(\d+(?:\.\d{2})?)\s*(?:dollar|dollars?)/i);
|
||||
if (amountMatch) {
|
||||
entities.amount = parseFloat(amountMatch[1] || amountMatch[2]);
|
||||
}
|
||||
|
||||
// Extract source/description
|
||||
const incomeMatch = text.match(/(?:income|received|earned)\s+(?:from\s+)?(.+?)(?:\s+\$|\s+\d+|$)/i);
|
||||
if (incomeMatch) {
|
||||
entities.description = incomeMatch[1].trim();
|
||||
}
|
||||
|
||||
entities.category = 'income';
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
async getModelStatus(): Promise<{ stt: boolean; tts: boolean; initialized: boolean }> {
|
||||
return {
|
||||
stt: await this.checkModelExists(this.sttModelPath),
|
||||
tts: await this.checkModelExists(this.ttsModelPath),
|
||||
initialized: this.isInitialized
|
||||
};
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
if (this.sttProcess) {
|
||||
this.sttProcess.kill();
|
||||
}
|
||||
if (this.ttsProcess) {
|
||||
this.ttsProcess.kill();
|
||||
}
|
||||
this.isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const voiceService = new VoiceService();
|
||||
@@ -0,0 +1,286 @@
|
||||
import { users, sessions, tasks, financialRecords, voiceCommands, aiInteractions, userPreferences, type User, type InsertUser, type Task, type InsertTask, type FinancialRecord, type InsertFinancialRecord, type Session, type VoiceCommand, type InsertVoiceCommand, type AIInteraction, type UserPreferences, type InsertUserPreferences } from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, desc, and, gte, lte, count } from "drizzle-orm";
|
||||
|
||||
export interface IStorage {
|
||||
// User management
|
||||
getUser(id: number): Promise<User | undefined>;
|
||||
getUserByEmail(email: string): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
updateUser(id: number, updates: Partial<User>): Promise<User | undefined>;
|
||||
|
||||
// Session management
|
||||
createSession(userId: number, token: string, expiresAt: Date): Promise<Session>;
|
||||
getSession(token: string): Promise<Session | undefined>;
|
||||
deleteSession(token: string): Promise<void>;
|
||||
|
||||
// Task management
|
||||
createTask(task: InsertTask): Promise<Task>;
|
||||
getTasks(userId: number, status?: string): Promise<Task[]>;
|
||||
getTask(id: number, userId: number): Promise<Task | undefined>;
|
||||
updateTask(id: number, userId: number, updates: Partial<Task>): Promise<Task | undefined>;
|
||||
deleteTask(id: number, userId: number): Promise<boolean>;
|
||||
|
||||
// Financial records
|
||||
createFinancialRecord(record: InsertFinancialRecord): Promise<FinancialRecord>;
|
||||
getFinancialRecords(userId: number, startDate?: Date, endDate?: Date): Promise<FinancialRecord[]>;
|
||||
getFinancialSummary(userId: number, startDate?: Date, endDate?: Date): Promise<{ income: number; expenses: number; net: number }>;
|
||||
updateFinancialRecord(id: number, userId: number, updates: Partial<FinancialRecord>): Promise<FinancialRecord | undefined>;
|
||||
deleteFinancialRecord(id: number, userId: number): Promise<boolean>;
|
||||
|
||||
// Voice commands
|
||||
createVoiceCommand(command: InsertVoiceCommand): Promise<VoiceCommand>;
|
||||
getVoiceCommands(userId: number, limit?: number): Promise<VoiceCommand[]>;
|
||||
|
||||
// AI interactions
|
||||
createAIInteraction(interaction: Omit<AIInteraction, 'id' | 'createdAt'>): Promise<AIInteraction>;
|
||||
getAIInteractions(userId?: number, type?: string, limit?: number): Promise<AIInteraction[]>;
|
||||
|
||||
// User preferences
|
||||
getUserPreferences(userId: number): Promise<UserPreferences | undefined>;
|
||||
updateUserPreferences(userId: number, preferences: Partial<InsertUserPreferences>): Promise<UserPreferences>;
|
||||
}
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
async getUser(id: number): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.id, id));
|
||||
return user || undefined;
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.email, email));
|
||||
return user || undefined;
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.username, username));
|
||||
return user || undefined;
|
||||
}
|
||||
|
||||
async createUser(insertUser: InsertUser): Promise<User> {
|
||||
const [user] = await db
|
||||
.insert(users)
|
||||
.values(insertUser)
|
||||
.returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
async updateUser(id: number, updates: Partial<User>): Promise<User | undefined> {
|
||||
const [user] = await db
|
||||
.update(users)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
return user || undefined;
|
||||
}
|
||||
|
||||
async createSession(userId: number, token: string, expiresAt: Date): Promise<Session> {
|
||||
const [session] = await db
|
||||
.insert(sessions)
|
||||
.values({ userId, token, expiresAt })
|
||||
.returning();
|
||||
return session;
|
||||
}
|
||||
|
||||
async getSession(token: string): Promise<Session | undefined> {
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(and(eq(sessions.token, token), gte(sessions.expiresAt, new Date())));
|
||||
return session || undefined;
|
||||
}
|
||||
|
||||
async deleteSession(token: string): Promise<void> {
|
||||
await db.delete(sessions).where(eq(sessions.token, token));
|
||||
}
|
||||
|
||||
async createTask(task: InsertTask): Promise<Task> {
|
||||
const [newTask] = await db
|
||||
.insert(tasks)
|
||||
.values(task)
|
||||
.returning();
|
||||
return newTask;
|
||||
}
|
||||
|
||||
async getTasks(userId: number, status?: string): Promise<Task[]> {
|
||||
const query = db.select().from(tasks).where(eq(tasks.userId, userId));
|
||||
|
||||
if (status) {
|
||||
query.where(and(eq(tasks.userId, userId), eq(tasks.status, status)));
|
||||
}
|
||||
|
||||
return await query.orderBy(desc(tasks.createdAt));
|
||||
}
|
||||
|
||||
async getTask(id: number, userId: number): Promise<Task | undefined> {
|
||||
const [task] = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, id), eq(tasks.userId, userId)));
|
||||
return task || undefined;
|
||||
}
|
||||
|
||||
async updateTask(id: number, userId: number, updates: Partial<Task>): Promise<Task | undefined> {
|
||||
const [task] = await db
|
||||
.update(tasks)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(eq(tasks.id, id), eq(tasks.userId, userId)))
|
||||
.returning();
|
||||
return task || undefined;
|
||||
}
|
||||
|
||||
async deleteTask(id: number, userId: number): Promise<boolean> {
|
||||
const result = await db
|
||||
.delete(tasks)
|
||||
.where(and(eq(tasks.id, id), eq(tasks.userId, userId)));
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
async createFinancialRecord(record: InsertFinancialRecord): Promise<FinancialRecord> {
|
||||
const [newRecord] = await db
|
||||
.insert(financialRecords)
|
||||
.values(record)
|
||||
.returning();
|
||||
return newRecord;
|
||||
}
|
||||
|
||||
async getFinancialRecords(userId: number, startDate?: Date, endDate?: Date): Promise<FinancialRecord[]> {
|
||||
let query = db.select().from(financialRecords).where(eq(financialRecords.userId, userId));
|
||||
|
||||
if (startDate && endDate) {
|
||||
query = query.where(
|
||||
and(
|
||||
eq(financialRecords.userId, userId),
|
||||
gte(financialRecords.date, startDate),
|
||||
lte(financialRecords.date, endDate)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return await query.orderBy(desc(financialRecords.date));
|
||||
}
|
||||
|
||||
async getFinancialSummary(userId: number, startDate?: Date, endDate?: Date): Promise<{ income: number; expenses: number; net: number }> {
|
||||
let query = db.select({
|
||||
type: financialRecords.type,
|
||||
amount: financialRecords.amount
|
||||
}).from(financialRecords).where(eq(financialRecords.userId, userId));
|
||||
|
||||
if (startDate && endDate) {
|
||||
query = query.where(
|
||||
and(
|
||||
eq(financialRecords.userId, userId),
|
||||
gte(financialRecords.date, startDate),
|
||||
lte(financialRecords.date, endDate)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const records = await query;
|
||||
|
||||
let income = 0;
|
||||
let expenses = 0;
|
||||
|
||||
records.forEach(record => {
|
||||
const amount = parseFloat(record.amount);
|
||||
if (record.type === 'income') {
|
||||
income += amount;
|
||||
} else {
|
||||
expenses += amount;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
income,
|
||||
expenses,
|
||||
net: income - expenses
|
||||
};
|
||||
}
|
||||
|
||||
async updateFinancialRecord(id: number, userId: number, updates: Partial<FinancialRecord>): Promise<FinancialRecord | undefined> {
|
||||
const [record] = await db
|
||||
.update(financialRecords)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(eq(financialRecords.id, id), eq(financialRecords.userId, userId)))
|
||||
.returning();
|
||||
return record || undefined;
|
||||
}
|
||||
|
||||
async deleteFinancialRecord(id: number, userId: number): Promise<boolean> {
|
||||
const result = await db
|
||||
.delete(financialRecords)
|
||||
.where(and(eq(financialRecords.id, id), eq(financialRecords.userId, userId)));
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
async createVoiceCommand(command: InsertVoiceCommand): Promise<VoiceCommand> {
|
||||
const [newCommand] = await db
|
||||
.insert(voiceCommands)
|
||||
.values(command)
|
||||
.returning();
|
||||
return newCommand;
|
||||
}
|
||||
|
||||
async getVoiceCommands(userId: number, limit: number = 50): Promise<VoiceCommand[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(voiceCommands)
|
||||
.where(eq(voiceCommands.userId, userId))
|
||||
.orderBy(desc(voiceCommands.createdAt))
|
||||
.limit(limit);
|
||||
}
|
||||
|
||||
async createAIInteraction(interaction: Omit<AIInteraction, 'id' | 'createdAt'>): Promise<AIInteraction> {
|
||||
const [newInteraction] = await db
|
||||
.insert(aiInteractions)
|
||||
.values(interaction)
|
||||
.returning();
|
||||
return newInteraction;
|
||||
}
|
||||
|
||||
async getAIInteractions(userId?: number, type?: string, limit: number = 50): Promise<AIInteraction[]> {
|
||||
let query = db.select().from(aiInteractions);
|
||||
|
||||
const conditions = [];
|
||||
if (userId) conditions.push(eq(aiInteractions.userId, userId));
|
||||
if (type) conditions.push(eq(aiInteractions.type, type));
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query = query.where(and(...conditions));
|
||||
}
|
||||
|
||||
return await query
|
||||
.orderBy(desc(aiInteractions.createdAt))
|
||||
.limit(limit);
|
||||
}
|
||||
|
||||
async getUserPreferences(userId: number): Promise<UserPreferences | undefined> {
|
||||
const [preferences] = await db
|
||||
.select()
|
||||
.from(userPreferences)
|
||||
.where(eq(userPreferences.userId, userId));
|
||||
return preferences || undefined;
|
||||
}
|
||||
|
||||
async updateUserPreferences(userId: number, preferences: Partial<InsertUserPreferences>): Promise<UserPreferences> {
|
||||
const existing = await this.getUserPreferences(userId);
|
||||
|
||||
if (existing) {
|
||||
const [updated] = await db
|
||||
.update(userPreferences)
|
||||
.set({ ...preferences, updatedAt: new Date() })
|
||||
.where(eq(userPreferences.userId, userId))
|
||||
.returning();
|
||||
return updated;
|
||||
} else {
|
||||
const [created] = await db
|
||||
.insert(userPreferences)
|
||||
.values({ userId, ...preferences })
|
||||
.returning();
|
||||
return created;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new DatabaseStorage();
|
||||
@@ -0,0 +1,85 @@
|
||||
import express, { type Express } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { createServer as createViteServer, createLogger } from "vite";
|
||||
import { type Server } from "http";
|
||||
import viteConfig from "../vite.config";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const viteLogger = createLogger();
|
||||
|
||||
export function log(message: string, source = "express") {
|
||||
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
console.log(`${formattedTime} [${source}] ${message}`);
|
||||
}
|
||||
|
||||
export async function setupVite(app: Express, server: Server) {
|
||||
const serverOptions = {
|
||||
middlewareMode: true,
|
||||
hmr: { server },
|
||||
allowedHosts: true,
|
||||
};
|
||||
|
||||
const vite = await createViteServer({
|
||||
...viteConfig,
|
||||
configFile: false,
|
||||
customLogger: {
|
||||
...viteLogger,
|
||||
error: (msg, options) => {
|
||||
viteLogger.error(msg, options);
|
||||
process.exit(1);
|
||||
},
|
||||
},
|
||||
server: serverOptions,
|
||||
appType: "custom",
|
||||
});
|
||||
|
||||
app.use(vite.middlewares);
|
||||
app.use("*", async (req, res, next) => {
|
||||
const url = req.originalUrl;
|
||||
|
||||
try {
|
||||
const clientTemplate = path.resolve(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"client",
|
||||
"index.html",
|
||||
);
|
||||
|
||||
// always reload the index.html file from disk incase it changes
|
||||
let template = await fs.promises.readFile(clientTemplate, "utf-8");
|
||||
template = template.replace(
|
||||
`src="/src/main.tsx"`,
|
||||
`src="/src/main.tsx?v=${nanoid()}"`,
|
||||
);
|
||||
const page = await vite.transformIndexHtml(url, template);
|
||||
res.status(200).set({ "Content-Type": "text/html" }).end(page);
|
||||
} catch (e) {
|
||||
vite.ssrFixStacktrace(e as Error);
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath = path.resolve(import.meta.dirname, "public");
|
||||
|
||||
if (!fs.existsSync(distPath)) {
|
||||
throw new Error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`,
|
||||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
|
||||
// fall through to index.html if the file doesn't exist
|
||||
app.use("*", (_req, res) => {
|
||||
res.sendFile(path.resolve(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user