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%
9.6 KB · 221 lines tsx
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      client/src/components/chat/custom-python-figure.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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";18import { Badge } from "@/components/ui/badge";19import { useState, useEffect } from "react";20import { ChevronDown, ChevronUp, Download } from "lucide-react";21import { Button } from "@/components/ui/button";2223interface FigureBatch {24  id: string;25  figures: string[]; // URLs (/figures/xxx.png) or base64 encoded images26  output?: string; // Text output from Python27  description?: string;28}2930function figureSrc(figure: string): string {31  // If it starts with / or http, it's a URL — use directly32  if (figure.startsWith('/') || figure.startsWith('http')) return figure;33  // Otherwise treat as base6434  return `data:image/png;base64,${figure}`;35}3637interface CustomPythonFigureGalleryProps {38  figureBatches: FigureBatch[]; // Array of figure batches39}4041export function CustomPythonFigure({ figureBatches }: CustomPythonFigureGalleryProps) {42  // Initialize with all batches expanded43  const [expandedBatches, setExpandedBatches] = useState<Set<string>>(44    new Set(figureBatches.map(b => b.id))45  );4647  // When new batches are added, expand them automatically48  useEffect(() => {49    const allIds = new Set(figureBatches.map(b => b.id));50    setExpandedBatches(allIds);51  }, [figureBatches.length]); // Update when number of batches changes5253  if (!figureBatches || figureBatches.length === 0) {54    return null;55  }5657  const totalFigures = figureBatches.reduce((sum, batch) => sum + batch.figures.length, 0);5859  const toggleBatch = (batchId: string) => {60    setExpandedBatches(prev => {61      const newSet = new Set(prev);62      if (newSet.has(batchId)) {63        newSet.delete(batchId);64      } else {65        newSet.add(batchId);66      }67      return newSet;68    });69  };7071  return (72    <Card className="w-full">73      <CardHeader>74        <CardTitle className="flex items-center gap-2">75          <span className="text-2xl">🐍</span>76          Galerie d'Analyses Python77          <Badge variant="secondary" className="ml-auto">78            {totalFigures} figure{totalFigures > 1 ? 's' : ''}79          </Badge>80        </CardTitle>81        <CardDescription>82          {figureBatches.length} analyse{figureBatches.length > 1 ? 's' : ''} Python générée{figureBatches.length > 1 ? 's' : ''}83        </CardDescription>84      </CardHeader>85      <CardContent className="space-y-4">86        {figureBatches.map((batch, batchIndex) => {87          const isExpanded = expandedBatches.has(batch.id);88          const batchNumber = batchIndex + 1;8990          return (91            <div key={batch.id} className="border rounded-lg overflow-hidden">92              {/* Batch Header - Collapsible */}93              <div94                className="flex items-center justify-between p-4 bg-muted/30 cursor-pointer hover:bg-muted/50 transition-colors"95                onClick={() => toggleBatch(batch.id)}96              >97                <div className="flex items-center gap-3">98                  <Badge variant="outline">#{batchNumber}</Badge>99                  <div>100                    <div className="font-semibold text-sm">101                      {batch.description || `Analyse Python #${batchNumber}`}102                    </div>103                    <div className="text-xs text-muted-foreground">104                      {batch.figures.length} figure{batch.figures.length > 1 ? 's' : ''}105                      {batch.output && ` • ${batch.output.split('\n').length} lignes de résultats`}106                    </div>107                  </div>108                </div>109                <Button variant="ghost" size="sm">110                  {isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}111                </Button>112              </div>113114              {/* Batch Content - Expandable */}115              {isExpanded && (116                <div className="p-4 space-y-4">117                  {/* Text output if available */}118                  {batch.output && batch.output.trim() && (119                    <div className="bg-muted/50 p-4 rounded-lg">120                      <h4 className="font-semibold mb-2 text-sm">📊 Résultats</h4>121                      <pre className="text-sm font-mono whitespace-pre-wrap overflow-x-auto">122                        {batch.output}123                      </pre>124                    </div>125                  )}126127                  {/* Figures Grid */}128                  <div className="space-y-3">129                    <div className="flex items-center justify-between">130                      <h4 className="font-semibold text-sm">131                        📈 Graphique{batch.figures.length > 1 ? 's' : ''}132                      </h4>133                      {batch.figures.length > 1 && (134                        <Button135                          variant="outline"136                          size="sm"137                          onClick={(e) => {138                            e.stopPropagation();139                            // Download all figures as zip would be nice, but for now just show button140                          }}141                          className="text-xs"142                        >143                          <Download className="w-3 h-3 mr-1" />144                          Tout télécharger145                        </Button>146                      )}147                    </div>148149                    <div className={`grid gap-4 ${batch.figures.length > 1 ? 'grid-cols-1 lg:grid-cols-2' : 'grid-cols-1'}`}>150                      {batch.figures.map((figure, figIndex) => {151                        const figureId = `${batch.id}-${figIndex}`;152153                        return (154                          <div key={figIndex} className="border rounded-lg overflow-hidden bg-card group relative">155                            {/* Figure ID badge for reference */}156                            <div className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">157                              <Badge variant="secondary" className="text-xs font-mono">158                                {figureId}159                              </Badge>160                            </div>161162                            <img163                              src={figureSrc(figure)}164                              alt={`Figure ${batchNumber}.${figIndex + 1}`}165                              className="w-full h-auto cursor-pointer"166                              style={{ maxHeight: '600px', objectFit: 'contain' }}167                              onError={(e) => {168                                const img = e.currentTarget;169                                if (!img.dataset.retried) {170                                  img.dataset.retried = 'true';171                                  // Retry with cache-busted URL172                                  const src = figureSrc(figure);173                                  img.src = src.includes('?') ? `${src}&t=${Date.now()}` : `${src}?t=${Date.now()}`;174                                } else {175                                  img.style.display = 'none';176                                  const fallback = img.parentElement?.querySelector('.figure-fallback');177                                  if (fallback) (fallback as HTMLElement).style.display = 'flex';178                                }179                              }}180                              onClick={() => {181                                const win = window.open();182                                if (win) {183                                  win.document.write(`<img src="${figureSrc(figure)}" style="max-width: 100%; height: auto;" />`);184                                  win.document.title = `Figure ${batchNumber}.${figIndex + 1}`;185                                }186                              }}187                            />188                            <div className="figure-fallback items-center justify-center p-8 bg-muted/30" style={{ display: 'none' }}>189                              <p className="text-sm text-muted-foreground">Figure non disponible</p>190                            </div>191192                            {/* Figure info footer */}193                            <div className="p-2 bg-muted/30 border-t flex items-center justify-between">194                              <div className="text-xs text-muted-foreground">195                                Figure {batchNumber}.{figIndex + 1}196                              </div>197                              <a198                                href={figureSrc(figure)}199                                download={`vibequant-figure-${figureId}.png`}200                                className="text-xs text-primary hover:underline flex items-center gap-1"201                                onClick={(e) => e.stopPropagation()}202                              >203                                <Download className="w-3 h-3" />204                                Télécharger205                              </a>206                            </div>207                          </div>208                        );209                      })}210                    </div>211                  </div>212                </div>213              )}214            </div>215          );216        })}217      </CardContent>218    </Card>219  );220}221