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%
12.7 KB · 357 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      client/src/hooks/useChatSession.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 { useState, useRef, useCallback } from "react";18import { useQuery, useQueryClient } from "@tanstack/react-query";19import { useChatMutation } from "./useChatMutation";20import { useAnalytics } from "./useAnalytics";21import type { SearchResult } from "@shared/types";22import type { AgentStep } from "@/components/chat/agent-steps";2324export interface SearchQuery {25  query: string;26  results?: SearchResult[];27}2829export interface ToolResult {30  tool: string;31  result: unknown;32}3334export interface ConversationMessage {35  question: string;36  answer: string;37}3839export interface FigureBatch {40  id: string;41  figures: string[];42  output?: string;43  description?: string;44}4546const FMP_TOOLS = [47  'get_company_profile', 'get_income_statement', 'get_balance_sheet',48  'get_cash_flow', 'get_key_metrics', 'get_financial_ratios',49  'get_stock_quote', 'get_historical_price', 'search_companies',50  'get_stock_peers', 'get_financial_news', 'get_rsi', 'get_macd',51  'get_ema', 'get_sma', 'get_adx', 'get_williams_r', 'get_cci',52  'get_stochastic', 'get_economic_calendar', 'get_earnings_calendar',53  'get_insider_trading', 'get_forex_quote', 'get_commodity_quotes',54  'get_treasury_rates', 'get_economic_indicator', 'get_earnings_surprises',55  'get_analyst_estimates', 'get_cot_report', 'get_press_releases',56  'get_dividend_history', 'get_stock_split_history', 'get_ipo_calendar',57  'create_plot', 'estimate_garch_volatility', 'calculate_var',58  'optimize_portfolio', 'analyze_risk_metrics', 'firecrawl_scrape',59  'firecrawl_crawl',60] as const;6162function resetChatState() {63  return {64    streamingAnswer: "",65    thinkingContent: "",66    sources: [] as SearchResult[],67    searchQueries: [] as SearchQuery[],68    toolResults: [] as ToolResult[],69    agentSteps: [] as AgentStep[],70    statusMessage: "",71    currentSearchIndex: 0,72    pythonCode: "",73    monteCarloResults: null as unknown,74    optionsPricingResults: null as unknown,75    customPythonFigures: [] as FigureBatch[],76    figureRegistry: new Map<string, string>(),77    figureUrls: [] as string[],78  };79}8081export type ClaudeModel =82  | 'claude-fable-5' | 'claude-opus-4-6' | 'claude-opus-4-7' | 'claude-opus-4-8' | 'claude-sonnet-4-6' | 'claude-haiku-4-5-20251001'83  | 'gpt-5.6-sol' | 'gpt-5.6-terra' | 'gpt-5.6-luna'84  | 'gpt-5.5' | 'gpt-5.4' | 'gpt-5.2' | 'gpt-5.1' | 'gpt-5' | 'gpt-5-mini' | 'gpt-5-nano' | 'gpt-4.1' | 'gpt-4.1-mini'85  | 'gemini-3.5-flash' | 'gemini-3.1-pro-preview' | 'gemini-3-flash-preview' | 'gemini-3.1-flash-lite';8687export function useChatSession() {88  const queryClient = useQueryClient();8990  // ─── Session state ──────────────────────────────────────91  const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);92  const [historyOpen, setHistoryOpen] = useState(false);93  const [conversationHistory, setConversationHistory] = useState<ConversationMessage[]>([]);94  const [currentQuestion, setCurrentQuestion] = useState("");95  const [selectedModel, setSelectedModel] = useState<ClaudeModel>(() => {96    const stored = localStorage.getItem('vquant-model-v2') as ClaudeModel | null;97    return stored ?? 'claude-fable-5';98  });99100  // ─── Chat response state ────────────────────────────────101  const [streamingAnswer, setStreamingAnswer] = useState("");102  const [thinkingContent, setThinkingContent] = useState("");103  const [sources, setSources] = useState<SearchResult[]>([]);104  const [searchQueries, setSearchQueries] = useState<SearchQuery[]>([]);105  const [toolResults, setToolResults] = useState<ToolResult[]>([]);106  const [agentSteps, setAgentSteps] = useState<AgentStep[]>([]);107  const [statusMessage, setStatusMessage] = useState("");108  const [currentSearchIndex, setCurrentSearchIndex] = useState(0);109  const [pythonCode, setPythonCode] = useState("");110  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Python executor returns untyped JSON111  const [monteCarloResults, setMonteCarloResults] = useState<any>(null);112  // eslint-disable-next-line @typescript-eslint/no-explicit-any113  const [optionsPricingResults, setOptionsPricingResults] = useState<any>(null);114  const [customPythonFigures, setCustomPythonFigures] = useState<FigureBatch[]>([]);115  const [figureRegistry, setFigureRegistry] = useState<Map<string, string>>(new Map());116  const [figureUrls, setFigureUrls] = useState<string[]>([]);117  const resultsRef = useRef<HTMLDivElement>(null);118119  // ─── Auth ───────────────────────────────────────────────120  const { data: authData } = useQuery<{ user: { id: string; displayName: string } | null }>({121    queryKey: ['/api/auth/me'],122  });123  const currentUser = authData?.user ?? null;124125  // ─── SSE event handler ──────────────────────────────────126  const chatMutation = useChatMutation((event) => {127    switch (event.type) {128      case 'text':129        setStreamingAnswer(prev => prev + event.content);130        break;131      case 'thinking':132        setThinkingContent(prev => prev + event.content);133        break;134      case 'sources':135        setSources(event.sources);136        break;137      case 'done':138        if (event.sessionId && !currentSessionId) {139          setCurrentSessionId(event.sessionId);140        }141        break;142      case 'status':143        setStatusMessage(event.message);144        break;145      case 'search_queries':146        setSearchQueries(event.queries.map((q: string) => ({ query: q })));147        setStatusMessage(event.message);148        break;149      case 'search_progress':150        setCurrentSearchIndex(event.index);151        setStatusMessage(`Search ${event.index}/${event.total}: ${event.query}`);152        break;153      case 'search_result':154        setSearchQueries(prev => {155          const updated = [...prev];156          if (updated[event.index - 1]) {157            updated[event.index - 1].results = event.results;158          }159          return updated;160        });161        break;162      case 'tool_start':163        setAgentSteps(prev => [...prev, {164          tool: event.tool,165          status: 'running',166          input: event.input,167        }]);168        break;169      case 'tool_complete':170        setAgentSteps(prev => prev.map(step =>171          step.tool === event.tool && step.status === 'running'172            ? { ...step, status: 'completed', duration: event.duration }173            : step,174        ));175        break;176      case 'tool_error':177        setAgentSteps(prev => prev.map(step =>178          step.tool === event.tool && step.status === 'running'179            ? { ...step, status: 'error', error: event.error }180            : step,181        ));182        break;183      case 'python_code':184        setPythonCode(event.code);185        break;186      case 'custom_python_figures': {187        const figId = event.figureId || `fig-${Date.now()}`;188        const isPlotTool = figId.startsWith('plot-');189190        if (!isPlotTool) {191          setCustomPythonFigures(prev => [...prev, {192            id: figId,193            figures: event.figures || [],194            output: event.output,195            description: event.description,196          }]);197        }198199        const urls = event.figureUrls || event.figures || [];200        if (urls.length > 0) {201          setFigureRegistry(prev => {202            const next = new Map(prev);203            urls.forEach((url: string, idx: number) => next.set(`${figId}-${idx}`, url));204            return next;205          });206          setFigureUrls(prev => [...prev, ...urls]);207        }208        break;209      }210      case 'tool_result':211        if (event.tool === 'run_monte_carlo_simulation') {212          setMonteCarloResults(event.result);213        } else if (event.tool === 'calculate_options_price') {214          setOptionsPricingResults(event.result);215        } else if (FMP_TOOLS.includes(event.tool)) {216          setToolResults(prev => [...prev, { tool: event.tool, result: event.result }]);217        }218        break;219    }220  });221222  // ─── Analytics ──────────────────────────────────────────223  const analyticsStatus = chatMutation.isPending ? 'generating' : chatMutation.isError ? 'error' : 'idle';224  useAnalytics(analyticsStatus, chatMutation.isPending ? currentQuestion : undefined);225226  // ─── Derived state ──────────────────────────────────────227  const hasSearched = streamingAnswer.length > 0 || chatMutation.isPending || chatMutation.isError || searchQueries.length > 0;228229  // ─── Actions ────────────────────────────────────────────230  const clearResponseState = useCallback(() => {231    setStreamingAnswer("");232    setThinkingContent("");233    setSources([]);234    setSearchQueries([]);235    setToolResults([]);236    setAgentSteps([]);237    setStatusMessage("");238    setCurrentSearchIndex(0);239    setPythonCode("");240    setMonteCarloResults(null);241    setOptionsPricingResults(null);242    setCustomPythonFigures([]);243    setFigureRegistry(new Map());244    setFigureUrls([]);245  }, []);246247  const handleSearch = useCallback((query: string, imageData?: string, imageMimeType?: string) => {248    if (currentQuestion && streamingAnswer) {249      setConversationHistory(prev => [...prev, {250        question: currentQuestion,251        answer: streamingAnswer,252      }]);253    }254255    setCurrentQuestion(query);256    clearResponseState();257258    chatMutation.mutate({259      query,260      history: currentQuestion && streamingAnswer261        ? [...conversationHistory, { question: currentQuestion, answer: streamingAnswer }]262        : conversationHistory,263      sessionId: currentSessionId || undefined,264      imageData,265      imageMimeType,266      model: selectedModel,267    });268  }, [currentQuestion, streamingAnswer, conversationHistory, currentSessionId, clearResponseState, chatMutation, selectedModel]);269270  const handleRetry = useCallback(() => {271    chatMutation.reset();272    clearResponseState();273  }, [chatMutation, clearResponseState]);274275  const handleNewConversation = useCallback(() => {276    setCurrentSessionId(null);277    setConversationHistory([]);278    setCurrentQuestion("");279    clearResponseState();280    chatMutation.reset();281  }, [clearResponseState, chatMutation]);282283  const handleLogout = useCallback(async () => {284    await fetch('/api/auth/logout', { method: 'POST' });285    queryClient.invalidateQueries({ queryKey: ['/api/auth/me'] });286    queryClient.invalidateQueries({ queryKey: ['/api/sessions'] });287  }, [queryClient]);288289  const handleLoadSession = useCallback(async (sessionId: string) => {290    try {291      const response = await fetch(`/api/sessions/${sessionId}`);292      if (!response.ok) return;293      const session = await response.json();294      setCurrentSessionId(session.sessionId);295      if (session.messages) {296        const msgs = typeof session.messages === 'string' ? JSON.parse(session.messages) : session.messages;297        setConversationHistory(msgs.map((m: { question?: string; answer?: string }) => ({298          question: m.question || '',299          answer: m.answer || '',300        })));301      }302      setHistoryOpen(false);303    } catch (error) {304      console.error('Failed to load session:', error);305    }306  }, []);307308  const handleModelChange = useCallback((model: ClaudeModel) => {309    setSelectedModel(model);310    localStorage.setItem('vquant-model-v2', model);311  }, []);312313  return {314    // Auth315    currentUser,316    handleLogout,317318    // Session319    currentSessionId,320    historyOpen,321    setHistoryOpen,322    handleLoadSession,323324    // Chat state325    conversationHistory,326    currentQuestion,327    streamingAnswer,328    thinkingContent,329    sources,330    searchQueries,331    toolResults,332    agentSteps,333    statusMessage,334    currentSearchIndex,335    pythonCode,336    monteCarloResults,337    optionsPricingResults,338    customPythonFigures,339    figureRegistry,340    figureUrls,341    resultsRef,342343    // Model selection344    selectedModel,345    handleModelChange,346347    // Mutation state348    chatMutation,349    hasSearched,350351    // Actions352    handleSearch,353    handleRetry,354    handleNewConversation,355  };356}357