FOUNDRY
C8 Platform
← Tasks

Wire dashboard to live AskRosetta API

completedcode_genP1monday-demo-push

Description

foundry-dashboard currently uses static data in jurisdiction-comparison.tsx and document-requirements.tsx. Wire both cards to call the live AskRosetta API endpoints (/api/classify, /api/duty) for real-time classification + duty. Add a "Live" toggle that switches between static demo data and live API calls. API base: askrosetta-mcp Cloud Run (EU 00039 or US 00034). Maintain static fallback for offline demos.
Assignee
VS Lu
Claimed By
Cloud Lu → Claude Sonnet
Created
20h ago
Completed
20h ago

Cost

Input Tokens
-
Output Tokens
-
Est. USD
-

Result

{
  "cost": {
    "input_tokens": 121,
    "estimated_usd": 0.093468,
    "output_tokens": 6207
  },
  "model": "claude-sonnet-4-20250514",
  "status": "success",
  "transport": "anthropic_api",
  "latency_ms": 69600,
  "response_text": "I'll help you wire the dashboard to the live AskRosetta API. Let me create the necessary components and utilities to handle both static and live data.\n\nFirst, let's create the API service and utilities:\n\n```typescript\n// lib/api-client.ts\nexport interface ClassificationResult {\n  jurisdiction: string;\n  confidence: number;\n  reasoning: string;\n}\n\nexport interface DutyResult {\n  dutyRate: number;\n  currency: string;\n  classification: string;\n  description: string;\n}\n\nexport interface ApiResponse<T> {\n  success: boolean;\n  data: T;\n  error?: string;\n}\n\nexport class AskRosettaClient {\n  private baseUrl: string;\n\n  constructor(region: 'eu' | 'us' = 'us') {\n    const regionCode = region === 'eu' ? '00039' : '00034';\n    this.baseUrl = `https://askrosetta-mcp-${regionCode}.a.run.app`;\n  }\n\n  async classify(description: string, origin?: string): Promise<ApiResponse<ClassificationResult>> {\n    try {\n      const response = await fetch(`${this.baseUrl}/api/classify`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ description, origin }),\n      });\n\n      if (!response.ok) {\n        throw new Error(`API request failed: ${response.statusText}`);\n      }\n\n      const data = await response.json();\n      return { success: true, data };\n    } catch (error) {\n      console.error('Classification API error:', error);\n      return { \n        success: false, \n        data: { jurisdiction: '', confidence: 0, reasoning: '' },\n        error: error instanceof Error ? error.message : 'Unknown error'\n      };\n    }\n  }\n\n  async getDuty(hsCode: string, origin: string, destination: string): Promise<ApiResponse<DutyResult>> {\n    try {\n      const response = await fetch(`${this.baseUrl}/api/duty`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ hsCode, origin, destination }),\n      });\n\n      if (!response.ok) {\n        throw new Error(`API request failed: ${response.statusText}`);\n      }\n\n      const data = await response.json();\n      return { success: true, data };\n    } catch (error) {\n      console.error('Duty API error:', error);\n      return { \n        success: false, \n        data: { dutyRate: 0, currency: 'USD', classification: '', description: '' },\n        error: error instanceof Error ? error.message : 'Unknown error'\n      };\n    }\n  }\n}\n\n// Create singleton instances\nexport const apiClientUS = new AskRosettaClient('us');\nexport const apiClientEU = new AskRosettaClient('eu');\n```\n\n```typescript\n// lib/static-data.ts\nimport { ClassificationResult, DutyResult } from './api-client';\n\nexport const staticClassificationData: ClassificationResult[] = [\n  {\n    jurisdiction: \"8517.12.00\",\n    confidence: 95,\n    reasoning: \"Smartphone with cellular capabilities, classified under telephones for cellular networks\"\n  },\n  {\n    jurisdiction: \"6203.42.40\",\n    confidence: 88,\n    reasoning: \"Men's cotton trousers, woven fabric, not of pile construction\"\n  },\n  {\n    jurisdiction: \"8471.30.01\",\n    confidence: 92,\n    reasoning: \"Portable automatic data processing machine, weighing not more than 10kg\"\n  }\n];\n\nexport const staticDutyData: DutyResult[] = [\n  {\n    dutyRate: 0,\n    currency: \"USD\",\n    classification: \"8517.12.00\",\n    description: \"Free trade rate for smartphones from China\"\n  },\n  {\n    dutyRate: 16.6,\n    currency: \"USD\",\n    classification: \"6203.42.40\",\n    description: \"Standard duty rate for cotton garments\"\n  },\n  {\n    dutyRate: 0,\n    currency: \"USD\",\n    classification: \"8471.30.01\",\n    description: \"Duty-free for laptop computers\"\n  }\n];\n\nexport const sampleProducts = [\n  \"Apple iPhone 15 Pro smartphone\",\n  \"Men's cotton blue jeans\",\n  \"MacBook Pro 16-inch laptop\"\n];\n\nexport const sampleOrigins = [\"CN\", \"MX\", \"DE\", \"JP\", \"VN\"];\nexport const sampleDestinations = [\"US\", \"EU\", \"CA\", \"AU\"];\n```\n\nNow let's create a live data toggle context:\n\n```typescript\n// contexts/live-data-context.tsx\n'use client';\n\nimport React, { createContext, useContext, useState } from 'react';\n\ninterface LiveDataContextType {\n  isLiveMode: boolean;\n  toggleLiveMode: () => void;\n  region: 'us' | 'eu';\n  setRegion: (region: 'us' | 'eu') => void;\n}\n\nconst LiveDataContext = createContext<LiveDataContextType | undefined>(undefined);\n\nexport function LiveDataProvider({ children }: { children: React.ReactNode }) {\n  const [isLiveMode, setIsLiveMode] = useState(false);\n  const [region, setRegion] = useState<'us' | 'eu'>('us');\n\n  const toggleLiveMode = () => {\n    setIsLiveMode(!isLiveMode);\n  };\n\n  return (\n    <LiveDataContext.Provider value={{ isLiveMode, toggleLiveMode, region, setRegion }}>\n      {children}\n    </LiveDataContext.Provider>\n  );\n}\n\nexport function useLiveData() {\n  const context = useContext(LiveDataContext);\n  if (!context) {\n    throw new Error('useLiveData must be used within a LiveDataProvider');\n  }\n  return context;\n}\n```\n\nNow let's update the jurisdiction comparison component:\n\n```typescript\n// components/jurisdiction-comparison.tsx\n'use client';\n\nimport React, { useState, useEffect } from 'react';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Badge } from '@/components/ui/badge';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport { Loader2, Zap, Database } from 'lucide-react';\nimport { useLiveData } from '@/contexts/live-data-context';\nimport { apiClientUS, apiClientEU, ClassificationResult } from '@/lib/api-client';\nimport { staticClassificationData, sampleProducts } from '@/lib/static-data';\n\nexport function JurisdictionComparison() {\n  const { isLiveMode } = useLiveData();\n  const [query, setQuery] = useState('');\n  const [loading, setLoading] = useState(false);\n  const [results, setResults] = useState<ClassificationResult[]>([]);\n  const [error, setError] = useState<string | null>(null);\n\n  const handleClassify = async () => {\n    if (!query.trim()) return;\n\n    setLoading(true);\n    setError(null);\n\n    try {\n      if (isLiveMode) {\n        // Call live API for both US and EU\n        const [usResult, euResult] = await Promise.all([\n          apiClientUS.classify(query),\n          apiClientEU.classify(query)\n        ]);\n\n        const newResults: ClassificationResult[] = [];\n        \n        if (usResult.success) {\n          newResults.push({ ...usResult.data, jurisdiction: `US: ${usResult.data.jurisdiction}` });\n        }\n        \n        if (euResult.success) {\n          newResults.push({ ...euResult.data, jurisdiction: `EU: ${euResult.data.jurisdiction}` });\n        }\n\n        if (newResults.length === 0) {\n          setError('Failed to get classification from both regions');\n        } else {\n          setResults(newResults);\n        }\n      } else {\n        // Use static data\n        const randomResult = staticClassificationData[Math.floor(Math.random() * staticClassificationData.length)];\n        setResults([\n          { ...randomResult, jurisdiction: `US: ${randomResult.jurisdiction}` },\n          { ...randomResult, jurisdiction: `EU: ${randomResult.jurisdiction}`, confidence: randomResult.confidence - 5 }\n        ]);\n      }\n    } catch (err) {\n      setError('An unexpected error occurred');\n      console.error('Classification error:', err);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const useSampleProduct = (product: string) => {\n    setQuery(product);\n  };\n\n  return (\n    <Card className=\"w-full\">\n      <CardHeader>\n        <CardTitle className=\"flex items-center gap-2\">\n          {isLiveMode ? <Zap className=\"h-5 w-5 text-green-500\" /> : <Database className=\"h-5 w-5 text-blue-500\" />}\n          Jurisdiction Comparison\n          <Badge variant={isLiveMode ? \"default\" : \"secondary\"}>\n            {isLiveMode ? \"Live\" : \"Demo\"}\n          </Badge>\n        </CardTitle>\n        <CardDescription>\n          Compare product classification across different trade jurisdictions\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        <div className=\"flex gap-2\">\n          <Input\n            placeholder=\"Enter product description...\"\n            value={query}\n            onChange={(e) => setQuery(e.target.value)}\n            onKeyPress={(e) => e.key === 'Enter' && handleClassify()}\n            className=\"flex-1\"\n          />\n          <Button \n            onClick={handleClassify} \n            disabled={loading || !query.trim()}\n          >\n            {loading ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : 'Classify'}\n          </Button>\n        </div>\n\n        {/* Sample Products */}\n        <div className=\"space-y-2\">\n          <p className=\"text-sm text-muted-foreground\">Try these examples:</p>\n          <div className=\"flex flex-wrap gap-2\">\n            {sampleProducts.map((product) => (\n              <Badge \n                key={product}\n                variant=\"outline\" \n                className=\"cursor-pointer hover:bg-secondary\"\n                onClick={() => useSampleProduct(product)}\n              >\n                {product}\n              </Badge>\n            ))}\n          </div>\n        </div>\n\n        {error && (\n          <Alert variant=\"destructive\">\n            <AlertDescription>{error}</AlertDescription>\n          </Alert>\n        )}\n\n        {/* Results */}\n        {results.length > 0 && (\n          <div className=\"space-y-3\">\n            <h4 className=\"font-semibold\">Classification Results</h4>\n            {results.map((result, index) => (\n              <Card key={index} className=\"border-l-4 border-l-blue-500\">\n                <CardContent className=\"pt-4\">\n                  <div className=\"space-y-2\">\n                    <div className=\"flex justify-between items-center\">\n                      <span className=\"font-mono text-sm bg-secondary px-2 py-1 rounded\">\n                        {result.jurisdiction}\n                      </span>\n                      <Badge variant={result.confidence > 90 ? \"default\" : \"secondary\"}>\n                        {result.confidence}% confidence\n                      </Badge>\n                    </div>\n                    <p className=\"text-sm text-muted-foreground\">\n                      {result.reasoning}\n                    </p>\n                  </div>\n                </CardContent>\n              </Card>\n            ))}\n          </div>\n        )}\n\n        {isLiveMode && (\n          <Alert>\n            <AlertDescription>\n              🔗 Connected to live AskRosetta API for real-time classification\n            </AlertDescription>\n          </Alert>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n```\n\nNow let's update the document requirements component:\n\n```typescript\n// components/document-requirements.tsx\n'use client';\n\nimport React, { useState, useEffect } from 'react';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { Badge } from '@/components/ui/badge';\nimport { Alert, AlertDescription } from '@/components/ui/alert';\nimport { Loader2, Zap, Database, DollarSign } from 'lucide-react';\nimport { useLiveData } from '@/contexts/live-data-context';\nimport { apiClientUS, apiClientEU, DutyResult } from '@/lib/api-client';\nimport { staticDutyData, sampleOrigins, sampleDestinations } from '@/lib/static-data';\n\nexport function DocumentRequirements() {\n  const { isLiveMode, region } = useLiveData();\n  const [hsCode, setHsCode] = useState('');\n  const [origin, setOrigin] = useState('');\n  const [destination, setDestination] = useState('');\n  const [loading, setLoading] = useState(false);\n  const [result, setResult] = useState<DutyResult | null>(null);\n  const [error, setError] = useState<string | null>(null);\n\n  const apiClient = region === 'eu' ? apiClientEU : apiClientUS;\n\n  const handleGetDuty = async () => {\n    if (!hsCode.trim() || !origin || !destination) return;\n\n    setLoading(true);\n    setError(null);\n\n    try {\n      if (isLiveMode) {\n        const response = await apiClient.getDuty(hsCode, origin, destination);\n        \n        if (response.success) {\n          setResult(response.data);\n        } else {\n          setError(response.error || 'Failed to get duty information');\n        }\n      } else {\n        // Use static data with some randomization\n        const randomResult = staticDutyData[Math.floor(Math.random() * staticDutyData.length)];\n        // Simulate some variation based on inputs\n        const variation = Math.random() * 0.1 - 0.05; // ±5%\n        setResult({\n          ...randomResult,\n          dutyRate: Math.max(0, randomResult.dutyRate * (1 + variation)),\n          classification: hsCode,\n          description: `Duty rate for ${hsCode} from ${origin} to ${destination}`\n        });\n      }\n    } catch (err) {\n      setError('An unexpected error occurred');\n      console.error('Duty lookup error:', err);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const resetForm = () => {\n    setHsCode('');\n    setOrigin('');\n    setDestination('');\n    setResult(null);\n    setError(null);\n  };\n\n  return (\n    <Card className=\"w-full\">\n      <CardHeader>\n        <CardTitle className=\"flex items-center gap-2\">\n          {isLiveMode ? <Zap className=\"h-5 w-5 text-green-500\" /> : <Database className=\"h-5 w-5 text-blue-500\" />}\n          Duty & Documentation\n          <Badge variant={isLiveMode ? \"default\" : \"secondary\"}>\n            {isLiveMode ? \"Live\" : \"Demo\"}\n          </Badge>\n        </CardTitle>\n        <CardDescription>\n          Get real-time duty rates and required documentation for trade compliance\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n          <div className=\"space-y-2\">\n            <label className=\"text-sm font-medium\">HS Code</label>\n            <Input\n              placeholder=\"e.g., 8517.12.00\"\n              value={hsCode}\n              onChange={(e) => setHsCode(e.target.value)}\n            />\n          </div>\n          \n          <div className=\"space-y-2\">\n            <label className=\"text-sm font-medium\">Origin</label>\n            <Select value={origin} onValueChange={setOrigin}>\n              <SelectTrigger>\n                <SelectValue placeholder=\"Select origin\" />\n              </SelectTrigger>\n              <SelectContent>\n                {sampleOrigins.map((country) => (\n                  <SelectItem key={country} value={country}>\n                    {country}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n\n          <div className=\"space-y-2\">\n            <label className=\"text-sm font-medium\">Destination</label>\n            <Select value={destination} onValueChange={setDestination}>\n              <SelectTrigger>\n                <SelectValue placeholder=\"Select destination\" />\n              </SelectTrigger>\n              <SelectContent>\n                {sampleDestinations.map((country) => (\n                  <SelectItem key={country} value={country}>\n                    {country}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n        </div>\n\n        <div className=\"flex gap-2\">\n          <Button \n            onClick={handleGetDuty} \n            disabled={loading || !hsCode.trim() || !origin || !destination}\n            className=\"flex-1\"\n          >\n            {loading ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : 'Get Duty Rate'}\n          </Button>\n          <Button variant=\"outline\" onClick={resetForm}>\n            Reset\n          </Button>\n        </div>\n\n        {error && (\n          <Alert variant=\"destructive\">\n            <AlertDescription>{error}</AlertDescription>\n          </Alert>\n        )}\n\n        {/* Results */}\n        {result && (\n          <Card className=\"border-l-4 border-l-green-500\">\n            <CardContent className=\"pt-4\">\n              <div className=\"space-y-4\">\n                <div className=\"flex items-center justify-between\">\n                  <h4 className=\"font-semibold flex items-center gap-2\">\n                    <DollarSign className=\"h-4 w-4\" />\n                    Duty Information\n                  </h4>\n                  <Badge variant=\"secondary\">\n                    {region.toUpperCase()} Region\n                  </Badge>\n                </div>\n                \n                <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n                  <div>\n                    <p className=\"text-sm text-muted-foreground\">Duty Rate</p>\n                    <p className=\"text-2xl font-bold text-green-600\">\n                      {result.dutyRate.toFixed(2)}%\n                    </p>\n                  </div>\n                  \n                  <div>\n                    <p className=\"text-sm text-muted-foreground\">Currency</p>\n                    <p className=\"text-lg font-semibold\">\n                      {result.currency}\n                    </p>\n                  </div>\n                </div>\n\n                <div>\n                  <p className=\"text-sm text-muted-foreground\">Classification</p>\n                  <span className=\"font-mono text-sm bg-secondary px-2 py-1 rounded\">\n                    {result.classification}\n                  </span>\n                </div>\n\n                <div>\n                  <p className=\"text-sm text-muted-foreground\">Description</p>\n                  <p className=\"text-sm\">\n                    {result.description}\n                  </p>\n                </div>\n              </div>\n            </CardContent>\n          </Card>\n        )}\n\n        {isLiveMode && (\n          <Alert>\n            <AlertDescription>\n              🔗 Connected to live AskRosetta API ({region.toUpperCase()}) for real-time duty calculations\n            </AlertDescription>\n          </Alert>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n```\n\nCreate a live mode toggle component:\n\n```typescript\n// components/live-mode-toggle.tsx\n'use client';\n\nimport React from 'react';\nimport { Switch } from '@/components/ui/switch';\nimport { Badge } from '@/components/ui/badge';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { useLiveData } from '@/contexts/live-data-context';\nimport { Zap, Database, Globe } from 'lucide-react';\n\nexport function LiveModeToggle() {\n  const { isLiveMode, toggleLiveMode, region, setRegion } = useLiveData();\n\n  return (\n    <div className=\"flex items-center gap-4 p-4 bg-card border rounded-lg\">\n      <div className=\"flex items-center gap-2\">\n        {isLiveMode ? (\n          <Zap className=\"h-4 w-4 text-green-500\" />\n        ) : (\n          <Database className=\"h-4 w-4 text-blue-500\" />\n        )}\n        <span className=\"text-sm font-medium\">\n          {isLiveMode ? 'Live API' : 'Demo Mode'}\n        </span>\n        <Switch\n          checked={isLiveMode}\n          onCheckedChange={toggleLiveMode}\n        />\n        <Badge variant={isLiveMode ? \"default\" : \"secondary\"}>\n          {isLiveMode ? \"Live\" : \"Static\"}\n        </Badge>\n      </div>\n\n      {isLiveMode && (\n        <div className=\"flex items-center gap-2 border-l pl-4\">\n          <Globe className=\"h-4 w-4 text-muted-foreground\" />\n          <span className=\"text-sm text-muted-foreground\">Region:</span>\n          <Select value={region} onValueChange={setRegion}>\n            <SelectTrigger className=\"w-20\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"us\">US</SelectItem>\n              <SelectItem value=\"eu\">EU</SelectItem>\n            </SelectContent>\n          </Select>\n        </div>\n      )}\n\n      <div className=\"ml-auto text-xs text-muted-foreground\">\n        {isLiveMode \n          ? `Connected to AskRosetta ${region.toUpperCase()} region`\n          : 'Using static demo data'\n        }\n      </div>\n    </div>\n  );\n}\n```\n\nFinally, update your main dashboard or layout to include the provider and toggle:\n\n```typescript\n// app/dashboard/layout.tsx (or wherever your dashboard is)\nimport { LiveDataProvider } from '@/contexts/live-data-context';\nimport { LiveModeToggle } from '@/components/live-mode-toggle';\n\nexport default function DashboardLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <LiveDataProvider>\n      <div className=\"space-y-6\">\n        <LiveModeToggle />\n        {children}\n      </div>\n    </LiveDataProvider>\n  );\n}\n```\n\nAnd update your main dashboard page:\n\n```typescript\n// app/dashboard/page.tsx\nimport { JurisdictionComparison } from '@/components/jurisdiction-comparison';\nimport { DocumentRequirements } from '@/components/document-requirements';\n\nexport default function DashboardPage() {\n  return (\n    <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n      <JurisdictionComparison />\n      <DocumentRequirements />\n    </div>\n  );\n}\n```\n\nThis implementation provides:\n\n1. **Live API Integration**: Connects to both US and EU AskRosetta endpoints\n2. **Live/Demo Toggle**: Users can switch between live API calls and static demo data\n3. **Region Selection**: Choose between US and EU API endpoints when in live mode\n4. **Error Handling**: Graceful fallbacks and error messages\n5. **Loading States**: Visual feedback during API calls\n6. **Static Fallback**: Always works offline for demos\n7. **Visual Indicators**: Clear badges and icons showing current mode\n8. **Sample Data**: Easy-to-use examples for testing\n\nThe dashboard now supports both static demo mode (perfect for presentations) and live mode (real-time data from AskRosetta APIs), with smooth switching between them."
}

Audit Trail (1)

20h agotask_createdDesktop Lu
Task ID: 22758345-25d8-4415-aee5-f53ec9b4cfd7