spb/vquant Public MIT
VibeQuant — AI-powered institutional-grade financial intelligence platform.
TypeScript 84.3%
Python 11.7%
JavaScript 1.6%
CSS 1.5%
HTML 0.7%
1/*2 * =============================================================================3 * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 * File: server/routes/reports.ts6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 * Website: https://www.spboucher.ai10 * Demo: https://www.vquant.ai11 * License: MIT (see LICENSE)12 *13 * Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import type { Express, Request, Response } from "express";18import { randomUUID } from "crypto";19import path from "path";20import fs from "fs";21import { storage } from "../storage";22import { convertToBeamerPDF } from "../services/beamerSlidesService";23import { logger } from "../utils/logger";2425export function registerReportRoutes(app: Express) {26 // ─── Shared Reports ─────────────────────────────────────2728 // Get all shared reports (explore page)29 app.get("/api/shared-reports", async (req: Request, res: Response) => {30 try {31 const reports = await storage.getAllSharedReports();3233 const simplifiedReports = reports34 .map(report => ({35 id: report.id,36 shareId: report.shareId,37 question: report.question,38 answer: report.answer.substring(0, 500),39 toolResults: report.toolResults ? JSON.parse(report.toolResults) : [],40 createdAt: report.createdAt,41 }))42 .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());4344 res.json(simplifiedReports);45 } catch (error) {46 logger.error("Get all shared reports error:", error);47 res.status(500).json({ error: "Failed to get shared reports" });48 }49 });5051 // Create a shareable report52 app.post("/api/share", async (req: Request, res: Response) => {53 try {54 const { question, answer, toolResults, sources, customPythonFigures } = req.body;5556 if (!question || !answer) {57 return res.status(400).json({ error: "Question and answer are required" });58 }5960 // Convert file-based figure URLs to base64 for persistence across deployments.61 // Figure files on disk (dist/public/figures/) are ephemeral and lost on redeploy,62 // so we read them now and store the base64 data directly in the database.63 let persistedFigures = customPythonFigures;64 if (Array.isArray(customPythonFigures)) {65 persistedFigures = customPythonFigures.map((batch: any) => {66 if (!Array.isArray(batch.figures)) return batch;67 const convertedFigures = batch.figures.map((fig: string) => {68 // Already base64 or data URI — keep as-is69 if (!fig.startsWith('/figures/') && !fig.startsWith('/plots/')) return fig;70 try {71 const filePath = path.join(process.cwd(), 'dist', 'public', fig);72 const buffer = fs.readFileSync(filePath);73 return buffer.toString('base64');74 } catch {75 logger.warn(`Could not read figure file for persistence: ${fig}`);76 return fig; // Keep URL as fallback77 }78 });79 return { ...batch, figures: convertedFigures };80 });81 }8283 const shareId = randomUUID().replace(/-/g, '').substring(0, 8);8485 const report = await storage.createSharedReport({86 shareId,87 question,88 answer,89 toolResults: toolResults ? JSON.stringify(toolResults) : null,90 sources: sources ? JSON.stringify(sources) : null,91 customPythonFigures: persistedFigures ? JSON.stringify(persistedFigures) : null,92 });9394 res.json({ shareId: report.shareId });95 } catch (error) {96 logger.error("Share error:", error);97 res.status(500).json({ error: "Failed to create shared report" });98 }99 });100101 // Get a shared report by ID102 app.get("/api/share/:shareId", async (req: Request, res: Response) => {103 try {104 const report = await storage.getSharedReportByShareId(req.params.shareId);105 if (!report) {106 return res.status(404).json({ error: "Shared report not found" });107 }108109 res.json({110 question: report.question,111 answer: report.answer,112 toolResults: report.toolResults ? JSON.parse(report.toolResults) : [],113 sources: report.sources ? JSON.parse(report.sources) : [],114 customPythonFigures: report.customPythonFigures ? JSON.parse(report.customPythonFigures) : null,115 createdAt: report.createdAt,116 });117 } catch (error) {118 logger.error("Get shared report error:", error);119 res.status(500).json({ error: "Failed to get shared report" });120 }121 });122123 // ─── Conversation Sessions ──────────────────────────────124125 // Get all sessions for current user126 app.get("/api/sessions", async (req: Request, res: Response) => {127 try {128 const userId = (req.session as any).userId;129 const sessions = await storage.getConversationSessions(userId);130131 res.json(sessions.map(session => ({132 sessionId: session.sessionId,133 title: session.title,134 createdAt: session.createdAt,135 updatedAt: session.updatedAt,136 })));137 } catch (error) {138 logger.error("Get sessions error:", error);139 res.status(500).json({ error: "Failed to get sessions" });140 }141 });142143 // Get specific session with messages144 app.get("/api/sessions/:sessionId", async (req: Request, res: Response) => {145 try {146 const userId = (req.session as any).userId;147 const { sessionId } = req.params;148149 let session;150 if (userId) {151 session = await storage.getConversationSessionByUserAndSessionId(sessionId, userId);152 } else {153 session = await storage.getConversationSession(sessionId);154 }155156 if (!session) {157 return res.status(404).json({ error: "Session not found" });158 }159160 res.json({161 sessionId: session.sessionId,162 title: session.title,163 messages: session.messages,164 createdAt: session.createdAt,165 updatedAt: session.updatedAt,166 });167 } catch (error) {168 logger.error("Get session error:", error);169 res.status(500).json({ error: "Failed to get session" });170 }171 });172173 // ─── Document Generation ────────────────────────────────174175 // PDF176 app.post("/api/generate-pdf", async (req: Request, res: Response) => {177 try {178 const { question, answer } = req.body;179 if (!question || !answer) {180 return res.status(400).json({ error: "Question and answer are required" });181 }182183 logger.info("Generating PDF with Puppeteer");184 const { generatePDFFromMarkdown } = await import("../services/pdfService");185 const pdfBuffer = await generatePDFFromMarkdown({ question, answer });186 const buffer = Buffer.isBuffer(pdfBuffer) ? pdfBuffer : Buffer.from(pdfBuffer);187188 const filename = `VQuant-${Date.now()}.pdf`;189 res.setHeader('Content-Type', 'application/pdf');190 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);191 res.setHeader('Content-Length', buffer.length);192 res.send(buffer);193 logger.success("PDF generated and sent successfully");194 } catch (error) {195 logger.error(`PDF generation error: ${(error as Error).message}`);196 res.status(500).json({ error: "Failed to generate PDF" });197 }198 });199200 // DOCX201 app.post("/api/generate-docx", async (req: Request, res: Response) => {202 try {203 const { question, answer } = req.body;204 if (!question || !answer) {205 return res.status(400).json({ error: "Question and answer are required" });206 }207208 logger.info("Generating DOCX with Pandoc");209 const { generateDOCXFromMarkdown } = await import("../services/pdfService");210 const docxBuffer = await generateDOCXFromMarkdown({ question, answer });211212 const filename = `VQuant-${Date.now()}.docx`;213 res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');214 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);215 res.setHeader('Content-Length', docxBuffer.length);216 res.send(docxBuffer);217 logger.success("DOCX generated and sent successfully");218 } catch (error) {219 logger.error(`DOCX generation error: ${(error as Error).message}`);220 res.status(500).json({ error: "Failed to generate DOCX" });221 }222 });223224 // Beamer slides225 app.post("/api/convert-to-slides", async (req: Request, res: Response) => {226 try {227 const { question, answer, author, company, figureUrls } = req.body;228 if (!question || !answer || !author) {229 return res.status(400).json({ error: "Question, answer, and author are required" });230 }231232 logger.info(`Generating Beamer PDF slides for: "${question.substring(0, 50)}..."`);233234 const pdfBuffer = await convertToBeamerPDF({235 question,236 answer,237 author,238 company: company || 'VQuant',239 figureUrls: figureUrls || [],240 });241242 if (!pdfBuffer || pdfBuffer.length === 0) {243 throw new Error('PDF buffer is empty or invalid');244 }245246 const filename = `VQuant-Presentation-${Date.now()}.pdf`;247 res.setHeader('Content-Type', 'application/pdf');248 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);249 res.setHeader('Content-Length', pdfBuffer.length.toString());250 res.setHeader('Cache-Control', 'no-cache');251 res.send(pdfBuffer);252 } catch (error) {253 logger.error(`Beamer slides error: ${(error as Error).message}`);254 if (!res.headersSent) {255 res.status(500).json({ error: "Failed to generate slides: " + (error as Error).message });256 }257 }258 });259260 // ─── File Downloads ─────────────────────────────────────261262 app.get("/api/download/:filename", async (req: Request, res: Response) => {263 try {264 const { filename } = req.params;265266 if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {267 return res.status(400).json({ error: "Invalid filename" });268 }269270 const downloadsDir = path.resolve('downloads');271 const filePath = path.join(downloadsDir, filename);272273 const fs = await import('fs/promises');274 try {275 await fs.access(filePath);276 } catch {277 return res.status(404).json({ error: "File not found" });278 }279280 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);281282 const ext = path.extname(filename).toLowerCase();283 const contentTypes: Record<string, string> = {284 '.csv': 'text/csv',285 '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',286 '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',287 '.json': 'application/json',288 '.txt': 'text/plain',289 };290 res.setHeader('Content-Type', contentTypes[ext] || 'application/octet-stream');291292 res.sendFile(filePath, (err) => {293 if (err) {294 logger.error(`Error sending file ${filename}: ${err.message}`);295 if (!res.headersSent) {296 res.status(500).json({ error: "Failed to send file" });297 }298 }299 });300 } catch (error) {301 logger.error(`Download error: ${(error as Error).message}`);302 res.status(500).json({ error: "Failed to download file" });303 }304 });305}306