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%
8.5 KB · 223 lines tsx
Raw Blame History
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