Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion web-ui/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import { RainbowProvider } from "./src/shared/providers/RainbowProvider";
import { BrokerProvider } from "./src/shared/providers/BrokerProvider";
import { DepositGuardProvider } from "./src/shared/providers/DepositGuardProvider";

export function Providers({ children }: { children: React.ReactNode }) {
return (
<RainbowProvider>
<BrokerProvider>{children}</BrokerProvider>
<BrokerProvider>
<DepositGuardProvider>{children}</DepositGuardProvider>
</BrokerProvider>
</RainbowProvider>
);
}
60 changes: 13 additions & 47 deletions web-ui/src/app/inference/chat/components/OptimizedChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import * as React from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import { useAccount } from "wagmi";
import { useConnectModal } from "@rainbow-me/rainbowkit";
import { useBroker } from "@/shared/providers/BrokerProvider";
import { useDepositGuard } from "@/shared/providers/DepositGuardProvider";
import { useChatHistory } from "../../../../shared/hooks/useChatHistory";
import { useProviderSearch } from "../../hooks/useProviderSearch";
import { useStreamingState } from "../../../../shared/hooks/useStreamingState";
Expand All @@ -27,23 +29,14 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import type { Provider } from "../../../../shared/types/broker";



interface Message {
role: "system" | "user" | "assistant";
content: string;
timestamp?: number;
chatId?: string;
isVerified?: boolean | null;
isVerifying?: boolean;
}
import type { Provider, Message } from "../../../../shared/types/broker";


export function OptimizedChatPage() {
const { isConnected, address } = useAccount();
const { broker, isInitializing, ledgerInfo, refreshLedgerInfo } = useBroker();
const { broker, readOnlyBroker, isInitializing, ledgerInfo, refreshLedgerInfo } = useBroker();
const { openConnectModal } = useConnectModal();
const { requestDeposit } = useDepositGuard();
const { toast } = useToast();

// Use toast for non-blocking errors
Expand All @@ -67,7 +60,7 @@ export function OptimizedChatPage() {
providerPendingRefund,
setSelectedProvider,
refreshProviderBalance,
} = useProviderManagement(broker);
} = useProviderManagement(broker, readOnlyBroker);

// Provider dropdown state (UI only)
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
Expand Down Expand Up @@ -171,6 +164,8 @@ export function OptimizedChatPage() {
setErrorWithTimeout,
isUserScrollingRef,
messagesEndRef,
openConnectModal,
requestDeposit,
});

// Handle editing a user message - truncates conversation and resends
Expand Down Expand Up @@ -445,38 +440,6 @@ export function OptimizedChatPage() {
// Note: handleDeposit is now handled globally in LayoutContent


if (!isConnected) {
return (
<div className="w-full">
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center">
<div className="flex items-center justify-center mb-6">
<div className="w-16 h-16 bg-purple-50 rounded-full flex items-center justify-center border border-purple-200">
<svg
className="w-8 h-8 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Wallet Not Connected
</h3>
<p className="text-gray-600">
Please connect your wallet to access AI inference features.
</p>
</div>
</div>
);
}

return (
<div className="w-full">
<div className="mb-1 sm:mb-3">
Expand Down Expand Up @@ -564,7 +527,10 @@ export function OptimizedChatPage() {
providerBalanceNeuron={providerBalanceNeuron}
providerPendingRefund={providerPendingRefund}
onAddFunds={() => {
// Use the existing top-up modal logic
if (!broker) {
openConnectModal?.();
return;
}
setShowTopUpModal(true);
}}
/>
Expand Down
20 changes: 14 additions & 6 deletions web-ui/src/app/inference/chat/components/ProviderSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import React, { useState, useEffect, useMemo } from 'react'
import { useAccount } from 'wagmi'
import type { Provider } from '../../../../shared/types/broker'
import { OFFICIAL_PROVIDERS } from '../../constants/providers'
import { copyToClipboard } from '@/lib/utils'
Expand All @@ -17,6 +18,7 @@ import {
getModelHealthStatus,
getHealthStatusColor,
getHealthStatusText,
type ProviderHealthStatus,
} from '@/shared/hooks/useProviderHealth'
import { formatNumber } from '@/shared/utils/formatNumber'

Expand Down Expand Up @@ -78,7 +80,7 @@ function MobileProviderCard({
isSelected: boolean
isRecentlyUsed: boolean
onSelect: () => void
healthData: Map<string, any[]>
healthData: Map<string, ProviderHealthStatus[]>
isLoadingHealth: boolean
}) {
const isTeeVerified =
Expand Down Expand Up @@ -217,16 +219,14 @@ export function ProviderSelector({
onAddFunds,
}: ProviderSelectorProps) {
const isMobile = useIsMobile()
const { isConnected } = useAccount()
const [mobileSearchQuery, setMobileSearchQuery] = useState('')

// Get health data using SWR hook (automatically cached across components)
const { healthData, isLoading: isLoadingHealth } = useProviderHealth()

// Recently used providers set for quick lookup
const recentlyUsedSet = useMemo(() => {
const used = getRecentlyUsedProviders()
return new Set(used)
}, [])
const recentlyUsedSet = new Set(getRecentlyUsedProviders())

// Filter out unverified providers - only show verified providers that can be used
const verifiedProviders = useMemo(() => {
Expand Down Expand Up @@ -715,7 +715,13 @@ export function ProviderSelector({
)}
{/* Right Section: Balance and Add Funds */}
<div className="flex items-center gap-1 sm:gap-1.5 px-1 sm:px-2 py-1 rounded-md">
<div
{!isConnected ? (
<span className="text-xs text-red-500 whitespace-nowrap px-1.5 py-1">
Wallet not connected
</span>
) : (
<>
<div
className={`flex items-center gap-1 sm:gap-1.5 px-1.5 sm:px-2 py-1 rounded-md text-xs ${
(providerBalanceNeuron !== null &&
providerBalanceNeuron === BigInt(0)) ||
Expand Down Expand Up @@ -864,6 +870,8 @@ export function ProviderSelector({
</svg>
<span className="hidden sm:inline">Add Funds</span>
</button>
</>
)}
</div>
</div>
)}
Expand Down
7 changes: 5 additions & 2 deletions web-ui/src/app/inference/chat/components/TopUpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import * as React from "react";
import { useState } from "react";
import type { ZGComputeNetworkBroker } from '@0glabs/0g-serving-broker';
import { a0giToNeuron } from "../../../../shared/utils/currency";
import { formatNumber } from "../../../../shared/utils/formatNumber";
import {
Expand Down Expand Up @@ -44,7 +45,7 @@ interface LedgerInfo {
interface TopUpModalProps {
isOpen: boolean;
onClose: () => void;
broker: any; // TODO: Replace with proper broker type when available
broker: ZGComputeNetworkBroker | null;
selectedProvider: Provider | null;
topUpAmount: string;
setTopUpAmount: (amount: string) => void;
Expand All @@ -53,7 +54,7 @@ interface TopUpModalProps {
providerBalance: number | null;
providerPendingRefund: number | null;
ledgerInfo: LedgerInfo | null;
refreshLedgerInfo: () => Promise<void>;
refreshLedgerInfo: () => Promise<LedgerInfo | null>;
refreshProviderBalance: () => Promise<void>;
setErrorWithTimeout: (error: string | null) => void;
}
Expand Down Expand Up @@ -275,6 +276,8 @@ export function TopUpModal({
<Button
onClick={handleTopUp}
disabled={
!broker ||
!selectedProvider ||
isTopping ||
!topUpAmount ||
parseFloat(topUpAmount) < MINIMUM_DEPOSITS.TOPUP_PROVIDER ||
Expand Down
104 changes: 104 additions & 0 deletions web-ui/src/app/inference/components/ModelCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client'

import * as React from 'react'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
MessageCircle,
Image,
Mic,
Wand2,
Users,
Shield,
ChevronRight,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import type { ModelSummary } from '@/shared/types/broker'

interface ModelCardProps {
model: ModelSummary
onClick: (model: ModelSummary) => void
}

const SERVICE_TYPE_CONFIG: Record<string, { label: string; icon: React.ElementType; color: string; hoverColor: string }> = {
chatbot: { label: 'Chatbot', icon: MessageCircle, color: 'bg-blue-100 text-blue-700', hoverColor: 'hover:bg-blue-100 hover:text-blue-700' },
'text-to-image': { label: 'Text to Image', icon: Image, color: 'bg-purple-100 text-purple-700', hoverColor: 'hover:bg-purple-100 hover:text-purple-700' },
'image-editing': { label: 'Image Editing', icon: Wand2, color: 'bg-pink-100 text-pink-700', hoverColor: 'hover:bg-pink-100 hover:text-pink-700' },
'speech-to-text': { label: 'Speech to Text', icon: Mic, color: 'bg-amber-100 text-amber-700', hoverColor: 'hover:bg-amber-100 hover:text-amber-700' },
}

function formatPriceRange(range: { min: number; max: number } | null): string | null {
if (!range) return null
if (range.min === range.max) {
return range.min.toFixed(2)
}
return `${range.min.toFixed(2)} - ${range.max.toFixed(2)}`
}

export function ModelCard({ model, onClick }: ModelCardProps) {
const typeConfig = SERVICE_TYPE_CONFIG[model.serviceType] || SERVICE_TYPE_CONFIG.chatbot
const TypeIcon = typeConfig.icon
const isImageService = model.serviceType === 'text-to-image' || model.serviceType === 'image-editing'

const priceDisplay = isImageService
? formatPriceRange(model.outputPriceRange)
: formatPriceRange(model.inputPriceRange)

const priceUnit = isImageService ? '0G/image' : '0G/1M tokens'

return (
<Card
className="relative group cursor-pointer hover:shadow-glow"
onClick={() => onClick(model)}
>
<CardContent className="p-5">
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
<h3 className="text-base font-semibold text-foreground truncate mb-2">
{model.displayName}
</h3>
<Badge
className={cn(
'border-0 px-2 py-0.5 text-xs font-medium flex items-center gap-1 w-fit',
typeConfig.color,
typeConfig.hoverColor,
)}
>
<TypeIcon className="h-3 w-3" />
{typeConfig.label}
</Badge>
</div>
<ChevronRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors flex-shrink-0 mt-1" />
</div>

{/* Stats */}
<div className="flex items-center gap-3 mt-4 text-sm">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Users className="h-3.5 w-3.5" />
<span>
{model.providerCount} provider{model.providerCount !== 1 ? 's' : ''}
</span>
</div>
{model.verifiedCount > 0 && (
<div className="flex items-center gap-1.5 text-green-600">
<Shield className="h-3.5 w-3.5" />
<span>{model.verifiedCount} verified</span>
</div>
)}
</div>

{/* Price range */}
{priceDisplay && (
<div className="mt-3 flex items-center gap-2 text-xs bg-secondary px-2.5 py-1.5 rounded-lg font-mono">
<span className="text-muted-foreground">Price:</span>
<span className="font-semibold text-foreground">
{priceDisplay}
</span>
<span className="text-muted-foreground">{priceUnit}</span>
</div>
)}
</CardContent>
</Card>
)
}
Loading