"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { useLanguage } from "@/contexts/LanguageContext"; import { getApiSettingsAsync } from "@/lib/storage"; import { getBaseUrlForModel, getApiKeyForModel } from "@/lib/api-helpers"; import { api } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { MessageCircle, X, Send, Loader2, Bot, User, Sparkles, AlertCircle, } from "lucide-react"; interface ChatMessage { role: "user" | "assistant"; content: string; } interface ReportChatProps { reports: any; ticker: string; analysisDate: string; } export function ReportChat({ reports, ticker, analysisDate }: ReportChatProps) { const { t, locale } = useLanguage(); const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const messagesEndRef = useRef(null); const inputRef = useRef(null); // Auto-scroll to bottom when messages change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isLoading]); // Focus input when panel opens useEffect(() => { if (isOpen) { setTimeout(() => inputRef.current?.focus(), 100); } }, [isOpen]); const handleSend = useCallback(async () => { const trimmed = input.trim(); if (!trimmed || isLoading) return; setError(null); const userMessage: ChatMessage = { role: "user", content: trimmed }; setMessages((prev) => [...prev, userMessage]); setInput(""); setIsLoading(true); try { // Get user's API settings const settings = await getApiSettingsAsync(); // Determine which model/key to use (prefer quick think model) // We'll try to detect the model from saved settings // Default to the first available key let model = "gpt-5-mini"; let apiKey = ""; let baseUrl = "https://api.openai.com/v1"; // Try each provider in order of preference const providers = [ { key: settings.openai_api_key, model: "gpt-5-mini", prefix: "gpt-" }, { key: settings.anthropic_api_key, model: "claude-sonnet-4-20250514", prefix: "claude-" }, { key: settings.google_api_key, model: "gemini-2.5-flash", prefix: "gemini-" }, { key: settings.grok_api_key, model: "grok-3-mini", prefix: "grok-" }, { key: settings.deepseek_api_key, model: "deepseek-chat", prefix: "deepseek-" }, { key: settings.qwen_api_key, model: "qwen-flash", prefix: "qwen" }, ]; for (const provider of providers) { if (provider.key && provider.key.trim() !== "") { apiKey = provider.key; model = provider.model; baseUrl = getBaseUrlForModel(model, settings.custom_base_url); break; } } // If custom endpoint is set, use that if (settings.custom_api_key && settings.custom_base_url) { apiKey = settings.custom_api_key; baseUrl = settings.custom_base_url; } if (!apiKey) { setError(t.chat?.noApiKey || "Please configure your API key in settings first."); setIsLoading(false); return; } // Build history from previous messages const history = messages.map((m) => ({ role: m.role, content: m.content, })); const response = await api.sendChatMessage({ message: trimmed, reports, ticker, analysis_date: analysisDate, history, model, api_key: apiKey, base_url: baseUrl, language: locale as "en" | "zh-TW", }); const assistantMessage: ChatMessage = { role: "assistant", content: response.reply, }; setMessages((prev) => [...prev, assistantMessage]); } catch (err: any) { console.error("Chat error:", err); const errorMsg = err?.response?.data?.detail || err?.message || (t.chat?.error || "Failed to get response. Please try again."); setError(errorMsg); } finally { setIsLoading(false); } }, [input, isLoading, messages, reports, ticker, analysisDate, locale, t]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return ( <> {/* Floating Chat Button */} {!isOpen && ( )} {/* Chat Panel */} {isOpen && (
{/* Header */}
{t.chat?.title || "Ask About Report"} — {ticker}
{/* Messages */}
{/* Empty state */} {messages.length === 0 && !isLoading && (

{t.chat?.emptyState || "Ask any question about this analysis report"}

{t.chat?.emptyHint || "e.g. \"What are the main risk factors?\""}

)} {/* Message list */} {messages.map((msg, i) => (
{/* Avatar */}
{msg.role === "user" ? ( ) : ( )}
{/* Bubble */}
{msg.role === "assistant" ? (
{msg.content}
) : (

{msg.content}

)}
))} {/* Loading indicator */} {isLoading && (
{t.chat?.thinking || "Thinking..."}
)} {/* Error message */} {error && (
{error}
)}
{/* Input Bar */}
setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder={t.chat?.placeholder || "Ask about this report..."} disabled={isLoading} className="flex-1 text-sm rounded-full border-gray-200 dark:border-gray-700 focus-visible:ring-purple-500" />
)} ); }