"use client"; import * as React from "react"; import { useState, useEffect, useRef, useCallback } from "react"; import { useAccount } from "wagmi"; import { useSearchParams, useRouter } from "next/navigation"; import { use0GBroker } from "../../../../hooks/use0GBroker"; import { useChatHistory } from "../../../../hooks/useChatHistory"; import { a0giToNeuron, neuronToA0gi } from "../../../../utils/currency"; import ReactMarkdown from "react-markdown"; interface Message { role: "system" | "user" | "assistant"; content: string; timestamp?: number; chatId?: string; isVerified?: boolean | null; isVerifying?: boolean; } interface Provider { address: string; model: string; name: string; verifiability: string; url?: string; endpoint?: string; inputPrice?: number; outputPrice?: number; inputPriceNeuron?: bigint; outputPriceNeuron?: bigint; } // Official providers based on the documentation const OFFICIAL_PROVIDERS: Provider[] = [ { address: "0xf07240Efa67755B5311bc75784a061eDB47165Dd", model: "llama-3.3-70b-instruct", name: "Llama 3.3 70B Instruct", verifiability: "TEE (TeeML)", }, { address: "0x3feE5a4dd5FDb8a32dDA97Bed899830605dBD9D3", model: "deepseek-r1-70b", name: "DeepSeek R1 70B", verifiability: "TEE (TeeML)", }, { address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", model: "llama-3.3-70b-instruct", name: "Llama 3.3 70B Instruct", verifiability: "TEE (TeeML)", }, ]; export function OptimizedChatPage() { const { isConnected, address } = useAccount(); const { broker, isInitializing, ledgerInfo, refreshLedgerInfo } = use0GBroker(); const searchParams = useSearchParams(); const router = useRouter(); const [providers, setProviders] = useState([]); const [selectedProvider, setSelectedProvider] = useState( null ); const [messages, setMessages] = useState([ { role: "system", content: "You are a helpful assistant that provides accurate information.", timestamp: Date.now(), }, ]); const [inputMessage, setInputMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isStreaming, setIsStreaming] = useState(false); const [error, setError] = useState(null); const errorTimeoutRef = useRef(null); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [serviceMetadata, setServiceMetadata] = useState<{ endpoint: string; model: string; } | null>(null); const [providerAcknowledged, setProviderAcknowledged] = useState< boolean | null >(null); const [isVerifyingProvider, setIsVerifyingProvider] = useState(false); // Note: Deposit modal is now handled globally in LayoutContent const [providerBalance, setProviderBalance] = useState(null); const [providerBalanceNeuron, setProviderBalanceNeuron] = useState(null); const [providerPendingRefund, setProviderPendingRefund] = useState(null); const [showFundingAlert, setShowFundingAlert] = useState(false); const [fundingAlertMessage, setFundingAlertMessage] = useState(""); const [showTopUpModal, setShowTopUpModal] = useState(false); const [topUpAmount, setTopUpAmount] = useState(""); const [isTopping, setIsTopping] = useState(false); const messagesEndRef = useRef(null); // Tutorial state const [showTutorial, setShowTutorial] = useState(false); const [tutorialStep, setTutorialStep] = useState<'verify' | 'top-up' | null>(null); // Chat history state const [showHistorySidebar, setShowHistorySidebar] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState<(Message & { sessionId: string })[]>([]); const [isSearching, setIsSearching] = useState(false); // Initialize chat history hook const chatHistory = useChatHistory({ walletAddress: address || '', providerAddress: selectedProvider?.address, autoSave: true, }); // Handle provider change - clear current session to start fresh const previousProviderRef = useRef(undefined); useEffect(() => { if (selectedProvider?.address && previousProviderRef.current !== undefined && previousProviderRef.current !== selectedProvider.address) { // Only clear when we actually switch providers, not on initial load setMessages([ { role: "system", content: "You are a helpful assistant that provides accurate information.", timestamp: Date.now(), }, ]); } previousProviderRef.current = selectedProvider?.address; }, [selectedProvider?.address]); // Custom setError function with auto-hide after 8 seconds const setErrorWithTimeout = (errorMessage: string | null) => { // Clear existing timeout if (errorTimeoutRef.current) { clearTimeout(errorTimeoutRef.current); errorTimeoutRef.current = null; } setError(errorMessage); // Set timeout to clear error after 8 seconds if (errorMessage) { errorTimeoutRef.current = setTimeout(() => { setError(null); errorTimeoutRef.current = null; }, 8000); } }; // Cleanup timeout on unmount useEffect(() => { return () => { if (errorTimeoutRef.current) { clearTimeout(errorTimeoutRef.current); } }; }, []); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Element; if (!target.closest(".provider-dropdown")) { setIsDropdownOpen(false); } }; if (isDropdownOpen) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [isDropdownOpen]); // Fetch real providers when broker is available useEffect(() => { const fetchProviders = async () => { if (broker) { try { // Use the broker to get real service list const services = await broker.inference.listService(); // Transform services to Provider format const transformedProviders: Provider[] = services.map( (service: unknown) => { // Type assertion for service properties const serviceObj = service as { provider?: string; model?: string; name?: string; verifiability?: string; url?: string; inputPrice?: bigint; outputPrice?: bigint; }; // Type guard to ensure service has the required properties const providerAddress = serviceObj.provider || ""; const rawModel = serviceObj.model || "Unknown Model"; const modelName = rawModel.includes('/') ? rawModel.split('/').slice(1).join('/') : rawModel; const rawProviderName = serviceObj.name || serviceObj.model || "Unknown Provider"; const providerName = rawProviderName.includes('/') ? rawProviderName.split('/').slice(1).join('/') : rawProviderName; const verifiability = serviceObj.verifiability || "TEE"; const serviceUrl = serviceObj.url || ""; // Convert prices from neuron to A0GI per million tokens const inputPrice = serviceObj.inputPrice ? neuronToA0gi(serviceObj.inputPrice * BigInt(1000000)) : undefined; const outputPrice = serviceObj.outputPrice ? neuronToA0gi(serviceObj.outputPrice * BigInt(1000000)) : undefined; return { address: providerAddress, model: modelName, name: providerName, verifiability: verifiability, url: serviceUrl, inputPrice, outputPrice, inputPriceNeuron: serviceObj.inputPrice, outputPriceNeuron: serviceObj.outputPrice, }; } ); setProviders(transformedProviders); // Check for provider parameter from URL const providerParam = searchParams.get('provider'); if (providerParam && !selectedProvider) { // Try to find the provider by address const targetProvider = transformedProviders.find( p => p.address.toLowerCase() === providerParam.toLowerCase() ); if (targetProvider) { setSelectedProvider(targetProvider); } else if (transformedProviders.length > 0) { // Fallback to first provider if specified provider not found setSelectedProvider(transformedProviders[0]); } } else if (!selectedProvider && transformedProviders.length > 0) { // Set the first provider as selected if none is selected setSelectedProvider(transformedProviders[0]); } } catch (err: unknown) { // Fallback to mock data if real data fetch fails setProviders(OFFICIAL_PROVIDERS); // Check for provider parameter from URL const providerParam = searchParams.get('provider'); if (providerParam && !selectedProvider) { // Try to find the provider by address const targetProvider = OFFICIAL_PROVIDERS.find( p => p.address.toLowerCase() === providerParam.toLowerCase() ); if (targetProvider) { setSelectedProvider(targetProvider); } else if (OFFICIAL_PROVIDERS.length > 0) { // Fallback to first provider if specified provider not found setSelectedProvider(OFFICIAL_PROVIDERS[0]); } } else if (!selectedProvider && OFFICIAL_PROVIDERS.length > 0) { setSelectedProvider(OFFICIAL_PROVIDERS[0]); } } } }; fetchProviders(); }, [broker, selectedProvider]); // Note: Global ledger check is now handled in LayoutContent component // Refresh ledger info when broker is available useEffect(() => { if (broker && refreshLedgerInfo) { refreshLedgerInfo(); } }, [broker, refreshLedgerInfo]); // Fetch service metadata when provider is selected useEffect(() => { const fetchServiceMetadata = async () => { if (broker && selectedProvider) { try { // Step 5.1: Get the request metadata const metadata = await broker.inference.getServiceMetadata( selectedProvider.address ); if (metadata?.endpoint && metadata?.model) { setServiceMetadata({ endpoint: metadata.endpoint, model: metadata.model }); } else { setServiceMetadata(null); } } catch (err: unknown) { setServiceMetadata(null); } } }; fetchServiceMetadata(); }, [broker, selectedProvider]); // Fetch provider acknowledgment status when provider is selected useEffect(() => { const fetchProviderAcknowledgment = async () => { if (broker && selectedProvider) { try { const acknowledged = await broker.inference.userAcknowledged( selectedProvider.address ); setProviderAcknowledged(acknowledged); // Check if we should show tutorial const tutorialKey = `tutorial_seen_${selectedProvider.address}`; if (!localStorage.getItem(tutorialKey) && showTutorial) { // If provider is already acknowledged, skip to top-up step if (acknowledged) { setTutorialStep('top-up'); } } } catch (err: unknown) { setProviderAcknowledged(false); } } }; fetchProviderAcknowledgment(); }, [broker, selectedProvider, showTutorial]); // Fetch provider balance when provider is selected useEffect(() => { const fetchProviderBalance = async () => { if (broker && selectedProvider) { try { const account = await broker.inference.getAccount(selectedProvider.address); if (account && account.balance) { const balanceInA0gi = neuronToA0gi(account.balance - account.pendingRefund); const pendingRefundInA0gi = neuronToA0gi(account.pendingRefund); setProviderBalance(balanceInA0gi); setProviderBalanceNeuron(account.balance); setProviderPendingRefund(pendingRefundInA0gi); } else { setProviderBalance(0); setProviderBalanceNeuron(BigInt(0)); setProviderPendingRefund(0); } } catch (err: unknown) { setProviderBalance(null); setProviderBalanceNeuron(null); setProviderPendingRefund(null); } } else if (!selectedProvider) { // Reset balance states when no provider is selected setProviderBalance(null); setProviderBalanceNeuron(null); setProviderPendingRefund(null); } }; fetchProviderBalance(); }, [broker, selectedProvider]); // Initialize tutorial when provider changes useEffect(() => { if (selectedProvider) { const tutorialKey = `tutorial_seen_${selectedProvider.address}`; const hasSeenTutorial = localStorage.getItem(tutorialKey); if (!hasSeenTutorial) { // Small delay to ensure UI is ready const timer = setTimeout(() => { setShowTutorial(true); if (providerAcknowledged === true) { setTutorialStep('top-up'); } else { setTutorialStep('verify'); } }, 800); return () => clearTimeout(timer); } } }, [selectedProvider, providerAcknowledged]); // Function to scroll to a specific message const scrollToMessage = useCallback((targetContent: string) => { const messageElements = document.querySelectorAll('[data-message-content]'); for (const element of messageElements) { if (element.getAttribute('data-message-content')?.includes(targetContent.substring(0, 50))) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Highlight the message temporarily element.classList.add('bg-yellow-100'); setTimeout(() => { element.classList.remove('bg-yellow-100'); }, 2000); break; } } }, []); // Function to handle history clicks with optional message targeting const handleHistoryClick = useCallback(async (sessionId: string, targetMessageContent?: string) => { // Clear any previous message targeting when clicking regular history if (!targetMessageContent) { lastTargetMessageRef.current = null; } try { // Reset loading/streaming states for history navigation setIsLoading(false); setIsStreaming(false); // Set flag to prevent auto-scrolling to bottom isLoadingHistoryRef.current = true; // Load session and get messages directly from database await chatHistory.loadSession(sessionId); // Import dbManager directly to get fresh data const { dbManager } = await import('../../../../lib/database'); const sessionMessages = await dbManager.getMessages(sessionId); // Convert database messages to UI format const historyMessages: Message[] = sessionMessages.map(msg => ({ role: msg.role, content: msg.content, timestamp: msg.timestamp, chatId: msg.session_id, // Use session_id for chatId isVerified: msg.is_verified, isVerifying: msg.is_verifying, })); // Add system message if needed const hasSystemMessage = historyMessages.some(msg => msg.role === 'system'); if (!hasSystemMessage && historyMessages.length > 0) { historyMessages.unshift({ role: "system", content: "You are a helpful assistant that provides accurate information.", timestamp: Date.now(), }); } setMessages(historyMessages); // If we have a target message, scroll to it after a delay if (targetMessageContent) { lastTargetMessageRef.current = targetMessageContent; setTimeout(() => { scrollToMessage(targetMessageContent); }, 300); } else { // Clear highlighting from previous targeted messages setTimeout(() => { const highlightedElements = document.querySelectorAll('.bg-yellow-100'); highlightedElements.forEach(el => el.classList.remove('bg-yellow-100')); }, 100); } // Reset flags setTimeout(() => { isLoadingHistoryRef.current = false; isUserScrollingRef.current = false; }, 200); } catch (err) { isLoadingHistoryRef.current = false; } }, [chatHistory, scrollToMessage]); // Simple debounced search using useEffect and setTimeout useEffect(() => { const timeoutId = setTimeout(async () => { if (!searchQuery.trim()) { setSearchResults([]); setIsSearching(false); return; } setIsSearching(true); try { const results = await chatHistory.searchMessages(searchQuery); const searchMessages: (Message & { sessionId: string })[] = results.map(msg => ({ role: msg.role, content: msg.content, timestamp: msg.timestamp, chatId: msg.chat_id, isVerified: msg.is_verified, isVerifying: msg.is_verifying, sessionId: msg.session_id || '', // Add session_id from database result })); setSearchResults(searchMessages); } catch (err) { setSearchResults([]); } finally { setIsSearching(false); } }, 300); return () => clearTimeout(timeoutId); }, [searchQuery]); // Only depend on searchQuery // Track sessions for reference const lastLoadedSessionRef = useRef(null); // Auto scroll to bottom when messages change (but not for verification updates or history navigation) const previousMessagesRef = useRef([]); const isUserScrollingRef = useRef(false); const isLoadingHistoryRef = useRef(false); const messagesContainerRef = useRef(null); const lastTargetMessageRef = useRef(null); const lastClickTimeRef = useRef(0); const lastClickedSessionRef = useRef(null); // Initialize click tracking on component mount useEffect(() => { lastClickTimeRef.current = 0; lastClickedSessionRef.current = null; lastTargetMessageRef.current = null; }, []); // Track user scroll behavior to stop auto-scroll when user manually scrolls up useEffect(() => { const messagesContainer = messagesContainerRef.current; if (!messagesContainer) return; const handleScroll = () => { const { scrollTop, scrollHeight, clientHeight } = messagesContainer; const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; if (!isNearBottom && isStreaming) { // User scrolled up during streaming, stop auto-scroll isUserScrollingRef.current = true; } else if (isNearBottom) { // User is back near bottom, resume auto-scroll isUserScrollingRef.current = false; } }; messagesContainer.addEventListener('scroll', handleScroll, { passive: true }); return () => messagesContainer.removeEventListener('scroll', handleScroll); }, [isStreaming]); useEffect(() => { const scrollToBottom = () => { if (isUserScrollingRef.current) return; // Don't scroll if user is manually scrolling messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; // Check if this is just a verification status update const isVerificationUpdate = () => { const prev = previousMessagesRef.current; if (prev.length !== messages.length) return false; // Check if only verification fields changed for (let i = 0; i < messages.length; i++) { const current = messages[i]; const previous = prev[i]; // If content, role, or timestamp changed, it's not just verification if (current.content !== previous.content || current.role !== previous.role || current.timestamp !== previous.timestamp || current.chatId !== previous.chatId) { return false; } } return true; }; // Don't auto-scroll if: // 1. It's just a verification update // 2. It's a history navigation (loading history) // 3. User is manually scrolling during streaming if (!isVerificationUpdate() && !isLoadingHistoryRef.current && !isUserScrollingRef.current) { const timeoutId = setTimeout(scrollToBottom, 100); // Update the ref after scrolling decision previousMessagesRef.current = [...messages]; return () => clearTimeout(timeoutId); } // Update the ref even if we don't scroll previousMessagesRef.current = [...messages]; }, [messages, isLoading, isStreaming]); const sendMessage = async () => { if (!inputMessage.trim() || !selectedProvider || !broker) { return; } // For now, let's add a simple demo response to test if the function works const userMessage: Message = { role: "user", content: inputMessage, timestamp: Date.now(), }; // Add user message to UI immediately setMessages((prev) => [...prev, userMessage]); // Save user message to database and get session ID (await to ensure session is created) let currentSessionForAssistant: string | null = null; try { currentSessionForAssistant = await chatHistory.addMessage({ role: userMessage.role, content: userMessage.content, chat_id: undefined, is_verified: null, is_verifying: false, }); } catch (err) { } setInputMessage(""); setIsLoading(true); setIsStreaming(true); setErrorWithTimeout(null); // Reset textarea height setTimeout(() => { const textarea = document.querySelector('textarea') as HTMLTextAreaElement; if (textarea) { textarea.style.height = '40px'; } }, 0); let firstContentReceived = false; try { // If serviceMetadata is not available, try to fetch it first let currentMetadata = serviceMetadata; if (!currentMetadata) { currentMetadata = await broker.inference.getServiceMetadata( selectedProvider.address ); if (currentMetadata?.endpoint && currentMetadata?.model) { setServiceMetadata({ endpoint: currentMetadata.endpoint, model: currentMetadata.model }); } else { setServiceMetadata(null); } if (!currentMetadata) { throw new Error("Failed to get service metadata"); } } // Step 5.2: Get the request headers (may trigger auto-funding) // Funding operations removed // Prepare the actual messages array that will be sent to the API const messagesToSend = [ ...messages .filter((m) => m.role !== "system") .map((m) => ({ role: m.role, content: m.content })), { role: userMessage.role, content: userMessage.content }, ]; let headers; try { headers = await broker.inference.getRequestHeaders( selectedProvider.address, JSON.stringify(messagesToSend) ); } catch (headerError) { throw headerError; } // Step 6: Send a request to the service use stream const { endpoint, model } = currentMetadata; const response = await fetch(`${endpoint}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", ...headers, }, body: JSON.stringify({ messages: [ ...messages .filter((m) => m.role !== "system") .map((m) => ({ role: m.role, content: m.content })), { role: userMessage.role, content: userMessage.content }, ], model: model, stream: true, }), }); if (!response.ok) { // Try to get detailed error message from response body let errorMessage = `HTTP error! status: ${response.status}`; try { const errorBody = await response.text(); if (errorBody) { // Try to parse as JSON first try { const errorJson = JSON.parse(errorBody); errorMessage = JSON.stringify(errorJson, null, 2); } catch { // If not JSON, use the raw text errorMessage = errorBody; } } } catch { // If can't read body, keep original message } throw new Error(errorMessage); } const reader = response.body?.getReader(); if (!reader) { throw new Error("Failed to get response reader"); } // Initialize streaming response const assistantMessage: Message = { role: "assistant", content: "", timestamp: Date.now(), isVerified: null, isVerifying: false, }; setMessages((prev) => [...prev, assistantMessage]); const decoder = new TextDecoder(); let buffer = ""; let chatId = ""; let completeContent = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { const data = line.slice(6); if (data === "[DONE]") continue; try { const parsed = JSON.parse(data); if (!chatId && parsed.id) { chatId = parsed.id; } const content = parsed.choices?.[0]?.delta?.content; if (content) { // Hide loading indicator on first content received if (!firstContentReceived) { setIsLoading(false); firstContentReceived = true; } completeContent += content; setMessages((prev) => prev.map((msg, index) => index === prev.length - 1 ? { ...msg, content: completeContent, chatId, isVerified: msg.isVerified, isVerifying: msg.isVerifying, } : msg ) ); // Trigger auto-scroll during streaming only if user isn't manually scrolling setTimeout(() => { if (!isUserScrollingRef.current) { messagesEndRef.current?.scrollIntoView({ behavior: "smooth", }); } }, 50); } } catch { // Skip invalid JSON } } } } } finally { reader.releaseLock(); } // Update final message with complete content and chatId setMessages((prev) => prev.map((msg, index) => index === prev.length - 1 ? { ...msg, content: completeContent, chatId, isVerified: msg.isVerified || null, isVerifying: msg.isVerifying || false, } : msg ) ); // Save assistant message to database in the background using the same session if (completeContent.trim() && currentSessionForAssistant) { // Directly save to database using the session ID we got from user message try { const { dbManager } = await import('../../../../lib/database'); await dbManager.saveMessage(currentSessionForAssistant, { role: "assistant", content: completeContent, timestamp: Date.now(), chat_id: chatId, is_verified: null, is_verifying: false, provider_address: selectedProvider?.address || '', }); } catch (err) { } } // Ensure loading is stopped even if no content was received if (!firstContentReceived) { setIsLoading(false); } // Always stop streaming when done setIsStreaming(false); } catch (err: unknown) { let errorMessage = "Failed to send message. Please try again."; if (err instanceof Error) { errorMessage = err.message; } else if (typeof err === 'string') { errorMessage = err; } else if (err && typeof err === 'object') { try { errorMessage = JSON.stringify(err, null, 2); } catch { errorMessage = String(err); } } setErrorWithTimeout(`Chat error: ${errorMessage}`); // Remove the loading message if it exists setMessages((prev) => prev.filter((msg) => msg.role !== "assistant" || msg.content !== "") ); // Ensure loading is stopped in case of error if (!firstContentReceived) { setIsLoading(false); } // Always stop streaming in case of error setIsStreaming(false); } }; // Step 7: Process the response (verification function) const verifyResponse = async (message: Message, messageIndex: number) => { if (!broker || !selectedProvider || !message.chatId) { return; } // Set verifying state and reset previous verification result setMessages((prev) => { const updated = prev.map((msg, index) => index === messageIndex ? { ...msg, isVerifying: true, isVerified: null } : msg ); return updated; }); // Force a re-render to ensure state change is visible await new Promise((resolve) => setTimeout(resolve, 100)); try { // Add minimum loading time to ensure user sees the loading effect const [isValid] = await Promise.all([ broker.inference.processResponse( selectedProvider.address, message.content, message.chatId ), new Promise((resolve) => setTimeout(resolve, 1000)), // Minimum 1 second loading ]); // Update verification result with visual feedback setMessages((prev) => { const updated = prev.map((msg, index) => index === messageIndex ? { ...msg, isVerified: isValid, isVerifying: false } : msg ); return updated; }); // Show visual feedback notification if (isValid) { } else { } } catch (err: unknown) { // Mark as verification failed with minimum loading time await new Promise((resolve) => setTimeout(resolve, 1000)); setMessages((prev) => { const updated = prev.map((msg, index) => index === messageIndex ? { ...msg, isVerified: false, isVerifying: false } : msg ); return updated; }); } }; // Remove clearChat function since we removed the Clear Chat button const startNewChat = async () => { // Create new session (this won't trigger sync due to hasManuallyLoadedSession flag) await chatHistory.createNewSession(); // Reset UI to clean state setMessages([ { role: "system", content: "You are a helpful assistant that provides accurate information.", timestamp: Date.now(), }, ]); setErrorWithTimeout(null); // Reset click tracking to ensure first history click works lastClickTimeRef.current = 0; lastClickedSessionRef.current = null; lastTargetMessageRef.current = null; // Update tracking to prevent sync on this new session lastLoadedSessionRef.current = chatHistory.currentSessionId; }; const verifyProvider = async () => { if (!broker || !selectedProvider) { return; } setIsVerifyingProvider(true); setErrorWithTimeout(null); try { await broker.inference.acknowledgeProviderSigner( selectedProvider.address ); // Refresh the acknowledgment status const acknowledged = await broker.inference.userAcknowledged( selectedProvider.address ); setProviderAcknowledged(acknowledged); // Refresh ledger info after successful verification if (acknowledged) { await refreshLedgerInfo(); } // Progress tutorial to top-up step if tutorial is active if (showTutorial && tutorialStep === 'verify' && acknowledged) { setTutorialStep('top-up'); } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : "Failed to verify provider. Please try again."; setErrorWithTimeout(`Verification error: ${errorMessage}`); } finally { setIsVerifyingProvider(false); } }; // Note: handleDeposit is now handled globally in LayoutContent const handleTopUp = async () => { if (!broker || !selectedProvider || !topUpAmount || parseFloat(topUpAmount) <= 0) { return; } setIsTopping(true); setErrorWithTimeout(null); try { const amountInA0gi = parseFloat(topUpAmount); const amountInNeuron = a0giToNeuron(amountInA0gi); // Call the transfer function with neuron amount await broker.ledger.transferFund( selectedProvider.address, 'inference', amountInNeuron ); // Refresh both ledger info and provider balance in parallel for better performance const [, account] = await Promise.all([ refreshLedgerInfo(), // Refresh ledger info to update available balance broker.inference.getAccount(selectedProvider.address) // Get updated provider account ]); // Update provider balance state if (account && account.balance) { const balanceInA0gi = neuronToA0gi(account.balance - account.pendingRefund); const pendingRefundInA0gi = neuronToA0gi(account.pendingRefund); setProviderBalance(balanceInA0gi); setProviderBalanceNeuron(account.balance); setProviderPendingRefund(pendingRefundInA0gi); } // Close modal and reset amount setShowTopUpModal(false); setTopUpAmount(""); // Complete tutorial if active if (showTutorial && tutorialStep === 'top-up') { setShowTutorial(false); setTutorialStep(null); // Mark tutorial as seen for this provider localStorage.setItem(`tutorial_seen_${selectedProvider.address}`, 'true'); } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : "Failed to top up. Please try again."; setErrorWithTimeout(`Top up error: ${errorMessage}`); } finally { setIsTopping(false); } }; if (!isConnected) { return (

Wallet Not Connected

Please connect your wallet to access AI inference features.

); } return (

Inference

Chat with AI models from decentralized providers

{error && (

Error

{(() => { try { // Try to parse as JSON if it looks like JSON if (error.trim().startsWith('{') && error.trim().endsWith('}')) { const parsed = JSON.parse(error); return JSON.stringify(parsed, null, 2); } return error; } catch { return error; } })()}

)} {showFundingAlert && (

Provider Funding

{fundingAlertMessage}

)}
{/* History Sidebar */} {showHistorySidebar && (

Chat History

{(isLoading || isStreaming) && (
AI responding...
)}
setSearchQuery(e.target.value)} disabled={isLoading || isStreaming} className={`w-full pl-8 pr-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-purple-500 ${ isLoading || isStreaming ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : '' }`} />
{/* Search Results */} {searchQuery ? (
{isSearching ? (
Searching...
) : searchResults.length === 0 ? (
No messages found for "{searchQuery}"
) : (
{searchResults.length} result(s) found
{searchResults.map((result, index) => (
{ if (result.sessionId && !isLoading && !isStreaming) { try { // Clear search first setSearchQuery(''); setSearchResults([]); // Load the session and scroll to the specific message await handleHistoryClick(result.sessionId, result.content); } catch (err) { } } }} >
{result.role === 'user' ? 'You' : 'Assistant'} • {' '} {result.timestamp ? new Date(result.timestamp).toLocaleDateString() : 'Unknown date'} View →
{result.content.length > 100 ? result.content.substring(0, 100) + '...' : result.content}
))}
)}
) : ( /* Session List */ chatHistory.sessions.length === 0 ? (
No chat history yet
) : (
{chatHistory.sessions.map((session) => (
{/* Delete button */}
))}
) )}
)} {/* Main Chat Area */}
{/* Chat Header with Provider Selection */}
{/* Provider Selection Dropdown */}
{/* Dropdown Menu */} {isDropdownOpen && (
{providers.map((provider) => { const isOfficial = OFFICIAL_PROVIDERS.some( (op) => op.address === provider.address ); return (
{ setSelectedProvider(provider); setIsDropdownOpen(false); }} className="px-3 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0" >
{provider.name} {provider.address.slice(0, 8)}... {provider.address.slice(-6)} {provider.verifiability} {isOfficial && ( 0G )}
{(provider.inputPrice !== undefined || provider.outputPrice !== undefined) && (
{provider.inputPrice !== undefined && ( Input: {provider.inputPrice.toFixed(2)}{" "} A0GI )} {provider.inputPrice !== undefined && provider.outputPrice !== undefined && ( · )} {provider.outputPrice !== undefined && ( Output: {provider.outputPrice.toFixed(2)}{" "} A0GI )} / million tokens
)}
); })}
)}
{/* Provider Info Bar - Redesigned */} {selectedProvider && (
{/* Left Section: Provider Status and Address */}
{/* Verification Status */} {providerAcknowledged !== null && ( {providerAcknowledged ? ( ) : ( )} {providerAcknowledged ? "Verified" : "Not Verified"} )} {/* Provider Address with Copy */}
{selectedProvider.address.slice(0, 6)}...{selectedProvider.address.slice(-4)}
{/* Copy Tooltip */}
Copy provider address
{/* Center Section: Price Info */} {(selectedProvider.inputPrice !== undefined || selectedProvider.outputPrice !== undefined) && (
{selectedProvider.inputPrice !== undefined && ( {selectedProvider.inputPrice.toFixed(2)} )} {selectedProvider.inputPrice !== undefined && selectedProvider.outputPrice !== undefined && ( / )} {selectedProvider.outputPrice !== undefined && ( {selectedProvider.outputPrice.toFixed(2)} )} A0GI/M
{/* Price Tooltip */}
Price per Million Tokens
{selectedProvider.inputPrice !== undefined && (
Input: {selectedProvider.inputPrice.toFixed(2)} A0GI
)} {selectedProvider.outputPrice !== undefined && (
Output: {selectedProvider.outputPrice.toFixed(2)} A0GI
)}
)} {/* Right Section: Funds and Actions */}
{/* Available Funds Display */}
{/* Icon based on fund status */} {(providerBalanceNeuron !== null && providerBalanceNeuron === BigInt(0)) || (providerBalance ?? 0) === 0 ? ( ) : providerBalanceNeuron !== null && selectedProvider.inputPriceNeuron !== undefined && selectedProvider.outputPriceNeuron !== undefined && providerBalanceNeuron <= BigInt(50000) * (selectedProvider.inputPriceNeuron + selectedProvider.outputPriceNeuron) ? ( ) : ( )} {(providerBalance ?? 0).toFixed(2)} A0GI
{/* Funds Tooltip */}
Available Funds
{providerBalance?.toFixed(18) ?? '0'} A0GI
{(providerBalanceNeuron !== null && providerBalanceNeuron === BigInt(0)) || (providerBalance ?? 0) === 0 ? (
❌ No funds - Add funds to use this provider
) : providerBalanceNeuron !== null && selectedProvider.inputPriceNeuron !== undefined && selectedProvider.outputPriceNeuron !== undefined && providerBalanceNeuron <= BigInt(50000) * (selectedProvider.inputPriceNeuron + selectedProvider.outputPriceNeuron) ? (
⚠️ Low funds - Provider may refuse service
) : providerBalanceNeuron !== null ? (
✅ Sufficient funds
) : (
Loading funds...
)}
{/* Add Funds Button */}
{/* Add Funds Tooltip */}
Add funds for the current provider service
)}
{/* History Tooltip */}
Toggle chat history
{/* New Tooltip */}
Start new chat
{/* Messages */}
{messages .map((message, originalIndex) => ({ message, originalIndex })) .filter(({ message }) => message.role !== "system") .map(({ message, originalIndex }, index) => (
{message.role === "assistant" ? (
(

{children}

), h2: ({ children }) => (

{children}

), h3: ({ children }) => (

{children}

), p: ({ children }) => (

{children}

), strong: ({ children }) => ( {children} ), em: ({ children }) => ( {children} ), code: ({ children }) => ( {children} ), pre: ({ children }) => (
                                  {children}
                                
), ul: ({ children }) => (
    {children}
), ol: ({ children }) => (
    {children}
), li: ({ children }) => (
  • {children}
  • ), blockquote: ({ children }) => (
    {children}
    ), a: ({ href, children }) => ( {children} ), }} > {message.content}
    ) : (
    {message.content}
    )}
    {message.timestamp && (
    {new Date(message.timestamp).toLocaleTimeString()} {/* Verification controls - only show for assistant messages that are complete */} {message.role === "assistant" && message.chatId && !isLoading && !isStreaming && (() => { const isExpired = message.timestamp && Date.now() - message.timestamp > 20 * 60 * 1000; // 20 minutes return (
    {/* Verification button for initial verification */} {!message.isVerifying && (message.isVerified === null || message.isVerified === undefined) && ( )} {/* Verification loading indicator */} {message.isVerifying && (
    Verifying...
    )} {/* Verification status display */} {!message.isVerifying && message.isVerified !== null && message.isVerified !== undefined && ( )}
    ); })()}
    )}
    ))} {isLoading && (
    AI is thinking...
    )} {/* Invisible element for auto-scroll */}
    {/* Input */}