SPB Git

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%
11.4 KB · 306 lines typescript
Raw Blame History
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