Skip to content

Commit 00e1325

Browse files
ymh357claude
andauthored
fix(web-ui): refactor broker to context provider with precision and U… (#162)
* fix(web-ui): refactor broker to context provider with precision and UX improvements Migrate use0GBroker hook to BrokerProvider context to eliminate duplicate broker instances. Fix isInitializing lifecycle by fetching ledger data directly via broker instance in initializeBroker, replacing fragile cross-effect handoff. Add cancellation-safe finally block. Key changes: - Extract BrokerProvider with Symbol-based cancellation and self-contained init lifecycle - Add neuronToA0giString for lossless BigInt-to-string conversion - Unify formatNumber across 5 components with optional full-precision mode - Fix XSS: replace dangerouslySetInnerHTML with React.ReactNode - Fix balance validation: use availableBalance instead of totalBalance - Fix scroll: increase threshold to 50px, use refs to avoid stale closures - Centralize minimum deposit constants in limits.ts - Extract ChainAwareDataCache to shared chainCache module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(web-ui): fix double loading on init and clean up dead code - Fix spurious chain switch during initial wallet connection by initializing currentChainIdRef as undefined and only recording chainId when wallet is connected - Reset currentChainIdRef on disconnect to prevent false chain switch on reconnection - Remove deprecated use0GBroker hook (no consumers remain) - Remove redundant refreshLedgerInfo effect from OptimizedChatPage - Simplify scroll handler by removing unnecessary refs and using proper useEffect deps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: minor type issue * fix(web-ui): fix ChatOnboarding cascade step-skip bug Replace props-driven auto-advance logic with localStorage-driven step tracking to prevent steps from cascading 1→2→3 on page load when hasProvider and hasBalance are already true. - Change localStorage key from 'chat-onboarding-completed' to 'chat-onboarding-step' storing "1"/"2"/"3"/"completed" - Remove hasProvider/hasBalance props and internal useEffect auto-advance logic from ChatOnboarding component - Move step state management to useChatOnboarding hook with advanceStep/completeOnboarding/resetOnboarding - Make step indicator dots clickable (forward-only persistence) - Remove unnecessary typeof window guards inside useEffect/callbacks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(web-ui): compare against persisted step to prevent localStorage regression advanceStep was comparing against React state which gets updated on dot navigation, causing localStorage to regress when viewing earlier steps then clicking forward (e.g. step 3 → dot 1 → dot 2 wrote "2"). Compare against localStorage value instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 351fffa commit 00e1325

34 files changed

+524
-526
lines changed

web-ui/Providers.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
"use client";
22

33
import { RainbowProvider } from "./src/shared/providers/RainbowProvider";
4+
import { BrokerProvider } from "./src/shared/providers/BrokerProvider";
45

56
export function Providers({ children }: { children: React.ReactNode }) {
6-
return <RainbowProvider>{children}</RainbowProvider>;
7+
return (
8+
<RainbowProvider>
9+
<BrokerProvider>{children}</BrokerProvider>
10+
</RainbowProvider>
11+
);
712
}

web-ui/src/app/inference/chat/components/ChatOnboarding.tsx

Lines changed: 36 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useState, useEffect } from 'react'
55
import { X, MessageCircle, Wallet, Send, ChevronRight } from 'lucide-react'
66
import { Button } from '@/components/ui/button'
77

8-
const ONBOARDING_STORAGE_KEY = 'chat-onboarding-completed'
8+
const ONBOARDING_STORAGE_KEY = 'chat-onboarding-step'
99

1010
interface OnboardingStep {
1111
id: number
@@ -42,63 +42,19 @@ const ONBOARDING_STEPS: OnboardingStep[] = [
4242
]
4343

4444
interface ChatOnboardingProps {
45-
hasProvider: boolean
46-
hasBalance: boolean
47-
onComplete: () => void
45+
currentStep: number
46+
onNext: () => void
47+
onSkip: () => void
48+
onStepClick: (step: number) => void
4849
}
4950

50-
export function ChatOnboarding({ hasProvider, hasBalance, onComplete }: ChatOnboardingProps) {
51-
const [currentStep, setCurrentStep] = useState(1)
52-
const [isVisible, setIsVisible] = useState(false)
53-
54-
// Check if onboarding should be shown
55-
useEffect(() => {
56-
if (typeof window !== 'undefined') {
57-
const completed = localStorage.getItem(ONBOARDING_STORAGE_KEY)
58-
if (!completed) {
59-
setIsVisible(true)
60-
}
61-
}
62-
}, [])
63-
64-
// Auto-advance steps based on user progress
65-
useEffect(() => {
66-
if (hasProvider && currentStep === 1) {
67-
setCurrentStep(2)
68-
}
69-
if (hasBalance && currentStep === 2) {
70-
setCurrentStep(3)
71-
}
72-
}, [hasProvider, hasBalance, currentStep])
73-
74-
const handleComplete = () => {
75-
if (typeof window !== 'undefined') {
76-
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'true')
77-
}
78-
setIsVisible(false)
79-
onComplete()
80-
}
81-
82-
const handleSkip = () => {
83-
handleComplete()
84-
}
85-
86-
const handleNext = () => {
87-
if (currentStep < ONBOARDING_STEPS.length) {
88-
setCurrentStep(currentStep + 1)
89-
} else {
90-
handleComplete()
91-
}
92-
}
93-
94-
if (!isVisible) return null
95-
51+
export function ChatOnboarding({ currentStep, onNext, onSkip, onStepClick }: ChatOnboardingProps) {
9652
const step = ONBOARDING_STEPS[currentStep - 1]
9753

9854
return (
9955
<>
10056
{/* Overlay */}
101-
<div className="fixed inset-0 bg-black/40 z-40" onClick={handleSkip} />
57+
<div className="fixed inset-0 bg-black/40 z-40" onClick={onSkip} />
10258

10359
{/* Onboarding Card */}
10460
<div className="fixed bottom-20 left-1/2 transform -translate-x-1/2 z-50 w-[90%] max-w-md">
@@ -128,7 +84,7 @@ export function ChatOnboarding({ hasProvider, hasBalance, onComplete }: ChatOnbo
12884
</div>
12985
</div>
13086
<button
131-
onClick={handleSkip}
87+
onClick={onSkip}
13288
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
13389
aria-label="Skip onboarding"
13490
>
@@ -144,13 +100,13 @@ export function ChatOnboarding({ hasProvider, hasBalance, onComplete }: ChatOnbo
144100
{/* Actions */}
145101
<div className="flex items-center justify-between">
146102
<button
147-
onClick={handleSkip}
103+
onClick={onSkip}
148104
className="text-sm text-gray-500 hover:text-gray-700 transition-colors"
149105
>
150106
Skip tutorial
151107
</button>
152108
<Button
153-
onClick={handleNext}
109+
onClick={onNext}
154110
size="sm"
155111
className="bg-purple-600 hover:bg-purple-700"
156112
>
@@ -165,9 +121,10 @@ export function ChatOnboarding({ hasProvider, hasBalance, onComplete }: ChatOnbo
165121
{/* Step indicators */}
166122
<div className="px-5 pb-4 flex justify-center gap-1.5">
167123
{ONBOARDING_STEPS.map((_, index) => (
168-
<div
124+
<button
169125
key={index}
170-
className={`h-1.5 rounded-full transition-all ${
126+
onClick={() => onStepClick(index + 1)}
127+
className={`h-1.5 rounded-full transition-all cursor-pointer ${
171128
index + 1 === currentStep
172129
? 'w-6 bg-purple-600'
173130
: index + 1 < currentStep
@@ -183,30 +140,40 @@ export function ChatOnboarding({ hasProvider, hasBalance, onComplete }: ChatOnbo
183140
)
184141
}
185142

186-
// Hook to check and reset onboarding
143+
// Hook to track onboarding step via localStorage
187144
export function useChatOnboarding() {
188145
const [showOnboarding, setShowOnboarding] = useState(false)
146+
const [currentStep, setCurrentStep] = useState(1)
189147

190148
useEffect(() => {
191-
if (typeof window !== 'undefined') {
192-
const completed = localStorage.getItem(ONBOARDING_STORAGE_KEY)
193-
setShowOnboarding(!completed)
149+
const stored = localStorage.getItem(ONBOARDING_STORAGE_KEY)
150+
if (stored === 'completed') {
151+
setShowOnboarding(false)
152+
} else {
153+
setShowOnboarding(true)
154+
setCurrentStep(stored ? parseInt(stored, 10) : 1)
194155
}
195156
}, [])
196157

197-
const resetOnboarding = () => {
198-
if (typeof window !== 'undefined') {
199-
localStorage.removeItem(ONBOARDING_STORAGE_KEY)
200-
setShowOnboarding(true)
158+
const advanceStep = (step: number) => {
159+
const stored = localStorage.getItem(ONBOARDING_STORAGE_KEY)
160+
const persistedStep = stored ? parseInt(stored, 10) : 1
161+
if (step > persistedStep) {
162+
localStorage.setItem(ONBOARDING_STORAGE_KEY, String(step))
201163
}
164+
setCurrentStep(step)
202165
}
203166

204167
const completeOnboarding = () => {
205-
if (typeof window !== 'undefined') {
206-
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'true')
207-
setShowOnboarding(false)
208-
}
168+
localStorage.setItem(ONBOARDING_STORAGE_KEY, 'completed')
169+
setShowOnboarding(false)
170+
}
171+
172+
const resetOnboarding = () => {
173+
localStorage.removeItem(ONBOARDING_STORAGE_KEY)
174+
setShowOnboarding(true)
175+
setCurrentStep(1)
209176
}
210177

211-
return { showOnboarding, resetOnboarding, completeOnboarding }
178+
return { showOnboarding, currentStep, advanceStep, completeOnboarding, resetOnboarding }
212179
}

web-ui/src/app/inference/chat/components/OptimizedChatPage.tsx

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import * as React from "react";
44
import { useState, useEffect, useRef, useCallback } from "react";
55
import { useAccount } from "wagmi";
6-
import { use0GBroker } from "../../../../shared/hooks/use0GBroker";
6+
import { useBroker } from "@/shared/providers/BrokerProvider";
77
import { useChatHistory } from "../../../../shared/hooks/useChatHistory";
88
import { useProviderSearch } from "../../hooks/useProviderSearch";
99
import { useStreamingState } from "../../../../shared/hooks/useStreamingState";
@@ -43,7 +43,7 @@ interface Message {
4343

4444
export function OptimizedChatPage() {
4545
const { isConnected, address } = useAccount();
46-
const { broker, isInitializing, ledgerInfo, refreshLedgerInfo } = use0GBroker();
46+
const { broker, isInitializing, ledgerInfo, refreshLedgerInfo } = useBroker();
4747
const { toast } = useToast();
4848

4949
// Use toast for non-blocking errors
@@ -110,7 +110,7 @@ export function OptimizedChatPage() {
110110
const { searchQuery, setSearchQuery, searchResults, isSearching, clearSearch } = useProviderSearch(chatHistory);
111111

112112
// Onboarding
113-
const { showOnboarding, completeOnboarding } = useChatOnboarding();
113+
const { showOnboarding, currentStep, advanceStep, completeOnboarding } = useChatOnboarding();
114114

115115
// Provider switch confirmation
116116
const [showSwitchWarning, setShowSwitchWarning] = useState(false);
@@ -237,14 +237,6 @@ export function OptimizedChatPage() {
237237

238238
// Note: Global ledger check is now handled in LayoutContent component
239239

240-
// Refresh ledger info when broker is available
241-
useEffect(() => {
242-
if (broker && refreshLedgerInfo) {
243-
refreshLedgerInfo();
244-
}
245-
}, [broker, refreshLedgerInfo]);
246-
247-
248240
// Function to scroll to a specific message
249241
const scrollToMessage = useCallback((targetContent: string) => {
250242
const messageElements = document.querySelectorAll('[data-message-content]');
@@ -361,20 +353,24 @@ export function OptimizedChatPage() {
361353

362354
const handleScroll = () => {
363355
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
364-
const isNearBottom = scrollHeight - scrollTop - clientHeight < CHAT_CONFIG.SCROLL_THRESHOLD;
365-
366-
if (!isNearBottom && isStreaming) {
367-
// User scrolled up during streaming, stop auto-scroll
368-
isUserScrollingRef.current = true;
369-
} else if (isNearBottom) {
370-
// User is back near bottom, resume auto-scroll
371-
isUserScrollingRef.current = false;
356+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
357+
358+
if (isStreaming || isLoading) {
359+
const isAtBottom = distanceFromBottom <= CHAT_CONFIG.SCROLL_THRESHOLD;
360+
isUserScrollingRef.current = !isAtBottom;
372361
}
373362
};
374363

375364
messagesContainer.addEventListener('scroll', handleScroll, { passive: true });
376365
return () => messagesContainer.removeEventListener('scroll', handleScroll);
377-
}, [isStreaming]);
366+
}, [isStreaming, isLoading]);
367+
368+
// Reset scroll tracking when streaming/loading ends
369+
useEffect(() => {
370+
if (!isStreaming && !isLoading) {
371+
isUserScrollingRef.current = false;
372+
}
373+
}, [isStreaming, isLoading]);
378374

379375
useEffect(() => {
380376
const scrollToBottom = () => {
@@ -648,9 +644,16 @@ export function OptimizedChatPage() {
648644
{/* First-time user onboarding */}
649645
{showOnboarding && (
650646
<ChatOnboarding
651-
hasProvider={!!selectedProvider}
652-
hasBalance={(providerBalance ?? 0) > 0}
653-
onComplete={completeOnboarding}
647+
currentStep={currentStep}
648+
onNext={() => {
649+
if (currentStep < 3) {
650+
advanceStep(currentStep + 1)
651+
} else {
652+
completeOnboarding()
653+
}
654+
}}
655+
onSkip={completeOnboarding}
656+
onStepClick={advanceStep}
654657
/>
655658
)}
656659

web-ui/src/app/inference/chat/components/ProviderSelector.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
getHealthStatusColor,
1919
getHealthStatusText,
2020
} from '@/shared/hooks/useProviderHealth'
21+
import { formatNumber } from '@/shared/utils/formatNumber'
2122

2223
// Helper to get recently used providers from localStorage
2324
const getRecentlyUsedProviders = (): string[] => {
@@ -44,25 +45,6 @@ function useIsMobile(breakpoint = 640) {
4445
return isMobile
4546
}
4647

47-
// Helper function to format numbers with appropriate precision
48-
const formatNumber = (num: number): string => {
49-
// Use toPrecision to maintain significant digits, then parseFloat to clean up
50-
const cleanValue = parseFloat(num.toPrecision(15))
51-
52-
// If the number is very small, show more decimal places
53-
if (Math.abs(cleanValue) < 0.000001) {
54-
return cleanValue.toFixed(12).replace(/\.?0+$/, '')
55-
}
56-
// For larger numbers, show fewer decimal places
57-
else if (Math.abs(cleanValue) < 0.01) {
58-
return cleanValue.toFixed(8).replace(/\.?0+$/, '')
59-
}
60-
// For normal sized numbers, show up to 6 decimal places
61-
else {
62-
return cleanValue.toFixed(6).replace(/\.?0+$/, '')
63-
}
64-
}
65-
6648
interface ProviderSelectorProps {
6749
// Provider selection
6850
providers: Provider[]

0 commit comments

Comments
 (0)