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/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