Skip to content

Frontend Architecture

WasiAI's frontend is built with Next.js 14, React 18, and Material UI. This document covers the key components and patterns.


Technology Stack

Technology Purpose
Next.js 14 React framework with App Router
React 18 UI library
TypeScript Type safety
Material UI Component library
wagmi Ethereum hooks
viem Ethereum utilities
RainbowKit Wallet connection
Thirdweb Social login
next-intl Internationalization

Project Structure

src/
├── app/                    # Next.js App Router
│   ├── [locale]/          # i18n routes (en/es)
│   │   ├── page.tsx       # Landing page
│   │   ├── models/        # Model catalog
│   │   ├── evm/models/[id]/ # Model detail
│   │   ├── licenses/      # User licenses
│   │   └── publish/wizard/ # Publish wizard
│   └── api/               # API routes
├── components/            # React components
│   ├── X402InferencePanel.tsx
│   ├── ModelCard.tsx
│   ├── ModelDetailView.tsx
│   └── ...
├── adapters/evm/          # Blockchain layer
│   ├── read.ts           # Read from contracts
│   └── write.ts          # Write to contracts
├── viewmodels/            # Data transformation
│   ├── types.ts
│   ├── factories.ts
│   └── adapters.ts
├── hooks/                 # Custom React hooks
├── contexts/              # React contexts
├── lib/                   # Utilities
├── config/                # Configuration
└── messages/              # i18n translations

Key Components

X402InferencePanel

The main component for pay-per-inference interactions.

Location: src/components/X402InferencePanel.tsx

Responsibilities: - Display input form - Handle x402 payment flow - Show inference results - Manage loading/error states

Props:

interface X402InferencePanelProps {
  modelId: string
  modelName: string
  agentId: number
  pricePerInference: string
  locale?: 'en' | 'es'
}

Key Functions: - runInference(): Main inference flow - buildAuthorization(): Create EIP-712 message - handleFeedback(): Submit reputation feedback

ModelCard

Displays a model in the catalog grid.

Location: src/components/ModelCard.tsx

Features: - Cover image with lazy loading - Price display - Category badge - Reputation indicator

ModelDetailView

Full model detail page content.

Location: src/components/ModelDetailView.tsx

Sections: - Hero with cover image - Description and metadata - Pricing information - X402InferencePanel - License purchase options

ConnectWalletModal

Hybrid wallet connection supporting social login and traditional wallets.

Location: src/components/ConnectWalletModal.tsx

Options: - Google, Apple, Email (Thirdweb) - MetaMask, WalletConnect (RainbowKit)


Data Flow

ViewModel Pattern

WasiAI uses ViewModels to transform raw data into UI-ready formats:

Blockchain Data → Adapter → ViewModel → Component
IPFS Metadata
Database Cache

Example:

// Raw data from multiple sources
const onChainData = await readContract(...)
const ipfsData = await fetchIPFS(...)
const cachedData = await queryDB(...)

// Transform to ViewModel
const modelVM = createModelViewModel({
  onChain: onChainData,
  ipfs: ipfsData,
  cached: cachedData
})

// Use in component
<ModelCard model={modelVM} />

State Management

State Type Solution
Server state React Query / SWR
Wallet state wagmi hooks
Form state React useState
Global UI React Context

Wallet Integration

RainbowKit Setup

// src/config/wagmi.ts
import { getDefaultConfig } from '@rainbow-me/rainbowkit'
import { avalancheFuji } from 'wagmi/chains'

export const config = getDefaultConfig({
  appName: 'WasiAI',
  projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID,
  chains: [avalancheFuji],
})

Thirdweb Social Login

// src/components/SocialLoginButtons.tsx
import { useConnect } from 'thirdweb/react'
import { inAppWallet } from 'thirdweb/wallets'

const wallet = inAppWallet()

// Google login
await wallet.connect({
  client,
  strategy: 'google'
})

Unified Connect Button

// src/components/UnifiedConnectButtonEvm.tsx
export function UnifiedConnectButton() {
  const { isConnected } = useAccount()

  if (isConnected) {
    return <AccountButton />
  }

  return (
    <Button onClick={openConnectModal}>
      Connect Wallet
    </Button>
  )
}

Internationalization

WasiAI supports English and Spanish via next-intl.

Message Files

src/messages/
├── en.json
└── es.json

Usage

import { useTranslations } from 'next-intl'

function MyComponent() {
  const t = useTranslations('ModelDetail')

  return <h1>{t('title')}</h1>
}

Locale Routing

/en/models     → English
/es/models     → Spanish

API Integration

Fetching Models

// From indexed cache (fast)
const models = await fetch('/api/indexed/models')

// From blockchain (authoritative)
const model = await readContract({
  address: MARKETPLACE_ADDRESS,
  abi: MarketplaceV3ABI,
  functionName: 'models',
  args: [modelId]
})

Running Inference

// src/components/X402InferencePanel.tsx
const runInference = async () => {
  // Step 1: Get payment requirements
  const res1 = await fetch(`/api/inference/${modelId}`, {
    method: 'POST',
    body: JSON.stringify({ input })
  })

  if (res1.status === 402) {
    // Step 2: Sign payment
    const payment = await signPayment(res1.json())

    // Step 3: Retry with payment
    const res2 = await fetch(`/api/inference/${modelId}`, {
      method: 'POST',
      headers: { 'X-PAYMENT': payment },
      body: JSON.stringify({ input })
    })

    return res2.json()
  }
}

Performance

Optimizations

Technique Implementation
Image optimization Next.js Image component
Code splitting Dynamic imports
Data caching SWR/React Query
Prefetching Link prefetch

Lazy Loading

import dynamic from 'next/dynamic'

const HeavyComponent = dynamic(
  () => import('./HeavyComponent'),
  { loading: () => <Skeleton /> }
)

Error Handling

Error Boundaries

// src/components/ErrorBoundary.tsx
class ErrorBoundary extends React.Component {
  state = { hasError: false }

  static getDerivedStateFromError(error) {
    return { hasError: true }
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback />
    }
    return this.props.children
  }
}

API Error Handling

try {
  const result = await runInference()
} catch (error) {
  if (error.code === 'INSUFFICIENT_FUNDS') {
    showToast('Insufficient USDC balance')
  } else if (error.code === 'USER_REJECTED') {
    showToast('Transaction cancelled')
  } else {
    showToast('Something went wrong')
  }
}

Testing

Component Testing

// __tests__/ModelCard.test.tsx
import { render, screen } from '@testing-library/react'
import { ModelCard } from '@/components/ModelCard'

test('displays model name', () => {
  render(<ModelCard model={mockModel} />)
  expect(screen.getByText('Test Model')).toBeInTheDocument()
})

E2E Testing

// e2e/inference.spec.ts
import { test, expect } from '@playwright/test'

test('can run inference', async ({ page }) => {
  await page.goto('/models/1')
  await page.fill('[data-testid="input"]', 'test input')
  await page.click('[data-testid="run-model"]')
  await expect(page.locator('[data-testid="result"]')).toBeVisible()
})