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: client/src/pages/shared-report.tsx6 *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 { useQuery } from "@tanstack/react-query";18import { useRoute, useLocation } from "wouter";19import { Sparkles, Loader2, ArrowLeft } from "lucide-react";20import { StreamingAnswer } from "@/components/streaming-answer";21import { ToolResults } from "@/components/chat/tool-results";22import { ResultCard } from "@/components/chat/result-card";23import { ThemeToggle } from "@/components/theme-toggle";24import { ThemeColorPicker } from "@/components/theme-color-picker";25import { Button } from "@/components/ui/button";26import { DownloadButtons } from "@/components/chat/download-buttons";27import { CustomPythonFigure } from "@/components/chat/custom-python-figure";28import type { SearchResult } from "@shared/types";29import { useRef, useMemo } from "react";3031interface SharedReportData {32 question: string;33 answer: string;34 toolResults: any[];35 sources: SearchResult[];36 customPythonFigures?: Array<{ id: string; figures: string[]; output?: string; description?: string }>;37 createdAt: string;38}3940export default function SharedReport() {41 const [, params] = useRoute("/share/:shareId");42 const [, setLocation] = useLocation();43 const shareId = params?.shareId;44 const contentRef = useRef<HTMLDivElement>(null);4546 const { data, isLoading, error } = useQuery<SharedReportData>({47 queryKey: ["/api/share", shareId],48 enabled: !!shareId,49 });5051 // Build figure registry for inline references52 const figureRegistry = useMemo(() => {53 const registry = new Map<string, string>();54 if (data?.customPythonFigures) {55 data.customPythonFigures.forEach(batch => {56 batch.figures.forEach((fig, idx) => {57 const figId = `${batch.id}-${idx}`;58 registry.set(figId, fig);59 });60 });61 }62 return registry;63 }, [data?.customPythonFigures]);6465 // Extract flat list of figure data URIs for markdown image resolution66 // Figures are stored as base64 in the DB — convert to data URIs so they67 // survive redeployments (file-based /figures/ URLs are ephemeral on disk).68 const figureUrls = useMemo(() => {69 if (!data?.customPythonFigures) return [];70 return data.customPythonFigures.flatMap(batch =>71 batch.figures.map(fig =>72 fig.startsWith('/') || fig.startsWith('http') || fig.startsWith('data:')73 ? fig74 : `data:image/png;base64,${fig}`75 )76 );77 }, [data?.customPythonFigures]);7879 if (isLoading) {80 return (81 <div className="min-h-screen bg-background flex items-center justify-center">82 <div className="text-center">83 <Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-primary" />84 <p className="text-muted-foreground">Chargement du rapport...</p>85 </div>86 </div>87 );88 }8990 if (error || !data) {91 return (92 <div className="min-h-screen bg-background flex items-center justify-center">93 <div className="text-center">94 <h2 className="text-2xl font-semibold mb-2">Rapport introuvable</h2>95 <p className="text-muted-foreground">Ce lien de partage n'existe pas ou a expiré</p>96 </div>97 </div>98 );99 }100101 return (102 <div className="min-h-screen bg-background relative overflow-hidden" data-testid="page-shared-report">103 {/* Subtle Background */}104 <div className="fixed inset-0 z-0 pointer-events-none">105 <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,hsl(var(--primary)/0.03),transparent_70%)]" />106 </div>107108 {/* Header */}109 <header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur-sm" data-testid="header-main">110 <div className="container flex h-14 items-center justify-between px-6">111 <div className="flex items-center gap-4">112 <Button113 variant="ghost"114 size="sm"115 onClick={() => setLocation('/')}116 className="hover:bg-muted transition-colors gap-2"117 data-testid="button-back-to-home"118 >119 <ArrowLeft className="w-4 h-4" />120 <span className="hidden sm:inline font-medium">Retour</span>121 </Button>122 <div className="cursor-pointer" data-testid="logo-container" onClick={() => setLocation('/')}>123 <div className="flex items-center gap-2.5">124 <Sparkles className="w-5 h-5 text-primary" data-testid="logo-icon" />125 <div className="hidden md:flex items-baseline gap-1.5">126 <span className="text-base font-bold text-foreground leading-tight">127 VQuant128 </span>129 </div>130 </div>131 </div>132 </div>133 <div className="flex items-center gap-3">134 <div className="h-8 w-px bg-border/50" />135 <ThemeColorPicker />136 <ThemeToggle />137 </div>138 </div>139 </header>140141 {/* Main Content */}142 <main className="container mx-auto px-4 py-12 md:py-20 relative z-10" data-testid="main-content">143 <div className="max-w-7xl mx-auto">144 {/* Branding */}145 <div className="mb-10 p-6 rounded-xl border border-border bg-card" data-testid="branding-section">146 <div className="flex items-center gap-4">147 <div className="p-2.5 rounded-lg bg-primary/10 border border-primary/20">148 <Sparkles className="w-6 h-6 text-primary" />149 </div>150 <div>151 <p className="text-sm font-medium text-muted-foreground">Rapport généré par</p>152 <p className="text-xl font-bold text-foreground tracking-tight">153 VQuant AI154 </p>155 </div>156 </div>157 <p className="text-base text-muted-foreground mt-4 font-medium">158 {new Date(data.createdAt).toLocaleDateString('fr-FR', {159 year: 'numeric',160 month: 'long',161 day: 'numeric',162 hour: '2-digit',163 minute: '2-digit'164 })}165 </p>166 </div>167168 {/* Question */}169 <div className="mb-6 p-4 rounded-lg bg-muted/50" data-testid="question-section">170 <p className="text-sm text-muted-foreground mb-1">Question</p>171 <p className="text-lg font-medium" data-testid="text-question">{data.question}</p>172 </div>173174 {/* Download Buttons */}175 <div className="flex gap-3 justify-center my-6">176 <DownloadButtons177 question={data.question}178 answer={data.answer}179 contentRef={contentRef}180 figureUrls={figureUrls}181 />182 </div>183184 {/* Content */}185 <div className="space-y-6" data-testid="content-section" ref={contentRef}>186 <StreamingAnswer187 content={data.answer}188 sources={data.sources}189 isStreaming={false}190 searchQueries={[]}191 statusMessage=""192 currentSearchIndex={0}193 figureRegistry={figureRegistry}194 figureUrls={figureUrls}195 />196197 <ToolResults toolResults={data.toolResults} />198199 {data.customPythonFigures && data.customPythonFigures.length > 0 && (200 <div className="max-w-5xl mx-auto mb-8">201 <CustomPythonFigure figureBatches={data.customPythonFigures} />202 </div>203 )}204205 {data.sources.length > 0 && (206 <div className="max-w-5xl mx-auto" data-testid="web-results-section">207 <h2 className="text-2xl font-semibold mb-6" data-testid="text-web-results-heading">208 Web Results209 </h2>210 <div className="grid gap-4" data-testid="results-grid">211 {data.sources.map((result: SearchResult, index: number) => (212 <ResultCard key={index} result={result} index={index} />213 ))}214 </div>215 </div>216 )}217 </div>218 </div>219 </main>220 </div>221 );222}223