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:
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¶
Usage¶
import { useTranslations } from 'next-intl'
function MyComponent() {
const t = useTranslations('ModelDetail')
return <h1>{t('title')}</h1>
}
Locale Routing¶
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()
})