mirror of
https://github.com/hanzoai/agents.git
synced 2026-10-30 18:48:36 +00:00
This example shipped a live tailwind pipeline (tailwind.config.ts, a postcss
config running the tailwind plugin, @tailwind directives in globals.css), ten
@radix-ui packages, and fourteen vendored shadcn components. Tailwind classes
and radix primitives are DOM-only, so the example taught readers a pattern that
cannot run on iOS or Android — and examples propagate.
Everything now renders through @hanzo/ui on the @hanzo/gui backend, which
compiles to React Native. Utility strings became style props on View/XStack/
YStack/Text; the vendored ui/ directory is gone because @hanzo/ui already
exports every primitive it re-implemented, with matching variant names.
Tailwind and @hanzo/ui cannot coexist: @hanzo/ui ships plain CSS cascade layers
and tailwind's postcss plugin rejects `@layer base` without `@tailwind base`.
The pipeline had to come out for the library to load at all, which is why this
is a whole-app change rather than a partial one.
Also removed EvaluationForm.tsx — 273 lines of tailwind nothing imported.
Platform moves the conversion required: React 18 -> 19 and Next 14 -> 15 for
@hanzo/ui's peer range, react-native aliased to react-native-web for the web
bundle, and <Hanzo> mounted in the root layout to install the theme.
Three defects found by rendering it rather than trusting the build:
- Tabs defaults to flexDirection row, so tab panels laid out beside the tab
list and spilled outside the metric cards
- TabsContent computes flex 1 1 0, which collapses to height 0 inside a
content-sized column; it needs flexBasis auto there
- CollapsibleTrigger has no asChild — it wraps children itself, so the claim
rows rendered as empty boxes
Measured: 1465 tailwind utility tokens across 22 files -> 0; 10 radix packages
-> 0; 14 vendored shadcn files -> 0. next build green, tsc --noEmit clean,
every tab and the claim expand/collapse verified in a real browser with no
console errors.
218 lines
6.9 KiB
TypeScript
218 lines
6.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { ChevronDown, Gavel } from 'lucide-react'
|
|
import {
|
|
Badge,
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
Separator,
|
|
Text,
|
|
XStack,
|
|
YStack,
|
|
} from '@hanzo/ui'
|
|
import type { BadgeVariant } from '@hanzo/ui'
|
|
import { Claim, EvaluationResult } from '@/lib/types'
|
|
|
|
interface ClaimBreakdownProps {
|
|
result: EvaluationResult
|
|
}
|
|
|
|
type FilterType = 'all' | 'grounded' | 'uncertain' | 'fabricated'
|
|
|
|
const FILTERS: { key: FilterType; label: string; on: BadgeVariant }[] = [
|
|
{ key: 'all', label: 'All', on: 'default' },
|
|
{ key: 'grounded', label: 'Grounded', on: 'default' },
|
|
{ key: 'uncertain', label: 'Uncertain', on: 'secondary' },
|
|
{ key: 'fabricated', label: 'Fabricated', on: 'destructive' },
|
|
]
|
|
|
|
const STATUS_VARIANT: Record<string, BadgeVariant> = {
|
|
grounded: 'default',
|
|
uncertain: 'secondary',
|
|
fabricated: 'destructive',
|
|
}
|
|
|
|
export function ClaimBreakdown({ result }: ClaimBreakdownProps) {
|
|
const [filter, setFilter] = useState<FilterType>('all')
|
|
|
|
const claims = result.faithfulness.claims || []
|
|
const filteredClaims = claims.filter((claim) => filter === 'all' || claim.status === filter)
|
|
const getCount = (status: string) => claims.filter((c) => c.status === status).length
|
|
|
|
return (
|
|
<YStack gap="$6">
|
|
<XStack alignItems="center" justifyContent="space-between" gap="$4" flexWrap="wrap">
|
|
<YStack>
|
|
<Text fontSize={18} fontWeight="500">
|
|
Claims Analysis
|
|
</Text>
|
|
<Text fontSize={14} color="$color11" marginTop={6}>
|
|
{claims.length} claims were identified and verified against the context.
|
|
</Text>
|
|
</YStack>
|
|
|
|
<XStack alignItems="center" gap="$2">
|
|
{FILTERS.map(({ key, label, on }) => (
|
|
<Badge
|
|
key={key}
|
|
variant={filter === key ? on : 'outline'}
|
|
onClick={() => setFilter(key)}
|
|
style={{ cursor: 'pointer' }}
|
|
>
|
|
{label} {key === 'all' ? claims.length : getCount(key)}
|
|
</Badge>
|
|
))}
|
|
</XStack>
|
|
</XStack>
|
|
|
|
<YStack gap="$4">
|
|
{filteredClaims.length === 0 ? (
|
|
<YStack alignItems="center" paddingVertical="$8" borderWidth={1} borderColor="$borderColor" borderRadius="$4" borderStyle="dashed">
|
|
<Text color="$color11">No claims found matching this filter.</Text>
|
|
</YStack>
|
|
) : (
|
|
filteredClaims.map((claim, index) => <ClaimItem key={claim.id || index} claim={claim} />)
|
|
)}
|
|
</YStack>
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
function Argument({
|
|
title,
|
|
badge,
|
|
argument,
|
|
kindLabel,
|
|
kind,
|
|
}: {
|
|
title: string
|
|
badge?: React.ReactNode
|
|
argument?: string
|
|
kindLabel?: string
|
|
kind?: string
|
|
}) {
|
|
return (
|
|
<Card flex={1} minWidth={280}>
|
|
<CardHeader>
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<CardTitle>{title}</CardTitle>
|
|
{badge}
|
|
</XStack>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{argument ? (
|
|
<YStack gap="$4">
|
|
<Text fontSize={14}>{argument}</Text>
|
|
{kind && (
|
|
<XStack alignItems="center" gap="$2">
|
|
<Text fontSize={12} fontWeight="500" color="$color11">
|
|
{kindLabel}:
|
|
</Text>
|
|
<Text fontSize={12} backgroundColor="$color3" paddingHorizontal="$2" paddingVertical={2} borderRadius="$2">
|
|
{kind.replace(/_/g, ' ')}
|
|
</Text>
|
|
</XStack>
|
|
)}
|
|
</YStack>
|
|
) : (
|
|
<Text fontSize={14} color="$color11" fontStyle="italic">
|
|
No {title.toLowerCase()} arguments.
|
|
</Text>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
function ClaimItem({ claim }: { claim: Claim }) {
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
|
|
return (
|
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
<Card overflow="hidden">
|
|
<CollapsibleTrigger width="100%" alignItems="stretch">
|
|
<YStack padding="$6" gap="$3" width="100%" hoverStyle={{ backgroundColor: '$color2' }}>
|
|
<XStack alignItems="flex-start" justifyContent="space-between" gap="$4">
|
|
<Text fontSize={14} fontWeight="500" flex={1} textAlign="left">
|
|
“{claim.text}”
|
|
</Text>
|
|
<YStack rotate={isOpen ? '180deg' : '0deg'} marginTop={2}>
|
|
<ChevronDown size={16} />
|
|
</YStack>
|
|
</XStack>
|
|
|
|
<XStack alignItems="center" gap="$3" flexWrap="wrap">
|
|
<Badge variant={STATUS_VARIANT[claim.status] || 'outline'}>{claim.status}</Badge>
|
|
|
|
{claim.type && <Badge variant="outline">{claim.type.replace(/_/g, ' ')}</Badge>}
|
|
|
|
{claim.evidence && (
|
|
<Text fontSize={12} color="$color11" numberOfLines={1} maxWidth={300}>
|
|
Evidence: {claim.evidence}
|
|
</Text>
|
|
)}
|
|
</XStack>
|
|
</YStack>
|
|
</CollapsibleTrigger>
|
|
|
|
<CollapsibleContent>
|
|
<Separator />
|
|
<YStack padding="$6" gap="$6">
|
|
<XStack gap="$6" flexWrap="wrap">
|
|
<Argument
|
|
title="Prosecution"
|
|
argument={claim.prosecution?.argument}
|
|
kindLabel="Type"
|
|
kind={claim.prosecution?.type}
|
|
badge={
|
|
claim.prosecution ? (
|
|
<Badge variant="destructive">{claim.prosecution.severity}</Badge>
|
|
) : undefined
|
|
}
|
|
/>
|
|
<Argument
|
|
title="Defense"
|
|
argument={claim.defense?.argument}
|
|
kindLabel="Support"
|
|
kind={claim.defense?.supportType}
|
|
badge={
|
|
claim.defense ? (
|
|
<Badge variant="secondary">
|
|
Strength: {Math.round(claim.defense.strength * 100)}%
|
|
</Badge>
|
|
) : undefined
|
|
}
|
|
/>
|
|
</XStack>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<XStack alignItems="center" justifyContent="space-between">
|
|
<XStack alignItems="center" gap="$2">
|
|
<Gavel size={16} />
|
|
<CardTitle>Judge Ruling</CardTitle>
|
|
</XStack>
|
|
{claim.judgeRuling && (
|
|
<Badge variant="outline">{claim.judgeRuling.verdict}</Badge>
|
|
)}
|
|
</XStack>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Text fontSize={14} color={claim.judgeRuling ? '$color' : '$color11'}>
|
|
{claim.judgeRuling ? claim.judgeRuling.reasoning : 'Pending ruling...'}
|
|
</Text>
|
|
</CardContent>
|
|
</Card>
|
|
</YStack>
|
|
</CollapsibleContent>
|
|
</Card>
|
|
</Collapsible>
|
|
)
|
|
}
|