Files
zeekay dc00ea93d0 rag_evaluation ui: tailwind + radix + shadcn out, @hanzo/ui in
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.
2026-08-13 12:29:55 -07:00

312 lines
12 KiB
TypeScript

'use client'
import { Scale, Target, Search, ScrollText, Check, AlertTriangle, X } from 'lucide-react'
import {
Badge,
Card,
CardContent,
CardHeader,
Progress,
ScrollView,
Separator,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Text,
XStack,
YStack,
} from '@hanzo/ui'
import { EvaluationResult } from '@/lib/types'
import { MarkdownContent } from './MarkdownContent'
interface MetricCardProps {
metric: 'faithfulness' | 'relevance' | 'hallucination' | 'constitutional'
result: EvaluationResult
}
const metricConfig = {
faithfulness: { icon: Scale, title: 'Faithfulness', subtitle: 'Adversarial Debate Pattern' },
relevance: { icon: Target, title: 'Relevance', subtitle: 'Multi-Jury Consensus' },
hallucination: { icon: Search, title: 'Hallucination', subtitle: 'Hybrid ML + LLM Verification' },
constitutional: {
icon: ScrollText,
title: 'Constitutional',
subtitle: 'Principles-Based Compliance',
},
}
function formatScore(score: number): string {
return `${Math.round(score * 100)}%`
}
function Stat({ value, label, tone }: { value: number; label: string; tone?: 'bad' | 'good' }) {
return (
<YStack flex={1} gap="$1" alignItems="center">
<Text
fontSize={24}
fontWeight="700"
color={tone === 'bad' ? '$destructive' : tone === 'good' ? '$primary' : '$color'}
>
{value}
</Text>
<Text fontSize={12} color="$color11">
{label}
</Text>
</YStack>
)
}
function Row({ label, value, tone }: { label: string; value: number; tone?: 'bad' | 'good' }) {
return (
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize={14} color="$color11">
{label}
</Text>
<Text
fontSize={14}
fontFamily="$mono"
color={tone === 'bad' ? '$destructive' : tone === 'good' ? '$primary' : '$color'}
>
{value}
</Text>
</XStack>
)
}
export function MetricCard({ metric, result }: MetricCardProps) {
const config = metricConfig[metric]
const Icon = config.icon
const data = result[metric]
return (
<Card flexGrow={1} flexBasis="45%" minWidth={320}>
<CardHeader>
<XStack alignItems="flex-start" justifyContent="space-between">
<XStack alignItems="center" gap="$3">
<YStack padding="$2" borderRadius="$4" backgroundColor="$color3">
<Icon size={20} />
</YStack>
<YStack>
<Text fontWeight="500">{config.title}</Text>
<Text fontSize={12} color="$color11">
{config.subtitle}
</Text>
</YStack>
</XStack>
<Text fontSize={24} fontWeight="700">
{formatScore(data.score)}
</Text>
</XStack>
</CardHeader>
<Separator />
<CardContent>
<Tabs defaultValue="summary" flexDirection="column" paddingTop="$4">
<TabsList width="100%" marginBottom="$4">
<TabsTrigger value="summary" flex={1}>
<Text fontSize={13}>Summary</Text>
</TabsTrigger>
<TabsTrigger value="details" flex={1}>
<Text fontSize={13}>Details</Text>
</TabsTrigger>
</TabsList>
<TabsContent value="summary" flexBasis="auto">
{metric === 'faithfulness' && (
<XStack gap="$3">
<Stat value={result.faithfulness.prosecutorIssues} label="Issues Found" />
<Stat value={result.faithfulness.defenderUpheld} label="Claims Upheld" />
<Stat
value={result.faithfulness.unfaithfulClaims}
label="Unfaithful"
tone={result.faithfulness.unfaithfulClaims > 0 ? 'bad' : 'good'}
/>
</XStack>
)}
{metric === 'relevance' && (
<YStack gap="$3">
{(['literal', 'intent', 'scope'] as const).map((type) => {
const score = result.relevance[
`${type}Score` as keyof typeof result.relevance
] as number
return (
<YStack key={type} gap={6}>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize={14} color="$color11" textTransform="capitalize">
{type}
</Text>
<Text fontSize={14} fontFamily="$mono">
{formatScore(score)}
</Text>
</XStack>
<Progress value={score * 100} height={8} />
</YStack>
)
})}
</YStack>
)}
{metric === 'hallucination' && (
<YStack gap="$3">
<XStack gap="$3">
<YStack flex={1} padding="$3" borderRadius="$4" backgroundColor="$color2">
<Stat value={result.hallucination.statementsAnalyzed} label="Statements" />
</YStack>
<YStack flex={1} padding="$3" borderRadius="$4" backgroundColor="$color2">
<Stat value={result.hallucination.mlVerified} label="ML Verified" />
</YStack>
</XStack>
<Row label="LLM Escalated" value={result.hallucination.llmEscalated} />
<Row
label="Fabrications Found"
value={result.hallucination.fabricationsFound}
tone={result.hallucination.fabricationsFound > 0 ? 'bad' : 'good'}
/>
</YStack>
)}
{metric === 'constitutional' && (
<YStack gap="$2">
{result.constitutional.principles.slice(0, 5).map((principle) => (
<XStack key={principle.id} alignItems="center" justifyContent="space-between" paddingVertical="$1">
<Text fontSize={14} color="$color11">
{principle.name.replace(/_/g, ' ')}
</Text>
<Badge
variant={
principle.passed
? 'default'
: principle.score >= 0.5
? 'secondary'
: 'destructive'
}
>
{principle.passed ? (
<Check size={12} />
) : principle.score >= 0.5 ? (
<AlertTriangle size={12} />
) : (
<X size={12} />
)}
{principle.passed ? 'passed' : principle.score >= 0.5 ? 'issue' : 'failed'}
</Badge>
</XStack>
))}
</YStack>
)}
</TabsContent>
<TabsContent value="details" flexBasis="auto">
<ScrollView maxHeight={192}>
{metric === 'faithfulness' && (
<YStack>
<Text fontSize={14} fontWeight="500" marginBottom="$2">
Debate Summary
</Text>
{result.faithfulness.debateSummary ? (
<MarkdownContent content={result.faithfulness.debateSummary} />
) : (
<Text fontSize={14} color="$color11">
No debate summary available
</Text>
)}
</YStack>
)}
{metric === 'relevance' && (
<YStack>
<Text fontSize={14} fontWeight="500" marginBottom="$2">
Jury Verdict
</Text>
{result.relevance.verdict ? (
<MarkdownContent content={result.relevance.verdict} />
) : (
<Text fontSize={14} color="$color11">
No verdict summary available
</Text>
)}
{result.relevance.disagreementLevel > 0.3 && (
<YStack marginTop="$3" padding="$2" borderRadius="$2" borderWidth={1} borderColor="$borderColor">
<Text fontSize={12}>
Note: Jury had {formatScore(result.relevance.disagreementLevel)} disagreement
</Text>
</YStack>
)}
</YStack>
)}
{metric === 'hallucination' && (
<YStack>
<Text fontSize={14} fontWeight="500" marginBottom="$2">
Verification Process
</Text>
<Text fontSize={14} color="$color11">
The hybrid verification process first used ML models (embeddings + NLI) to
verify {result.hallucination.mlVerified} statements.
{result.hallucination.llmEscalated > 0
? ` ${result.hallucination.llmEscalated} uncertain cases were escalated to LLM for deeper analysis.`
: ''}
</Text>
{result.hallucination.fabricationsFound > 0 && (
<YStack marginTop="$3" padding="$2" borderRadius="$2" borderWidth={1} borderColor="$destructive">
<Text fontSize={12} color="$destructive">
{result.hallucination.fabricationsFound} fabrication(s) detected
</Text>
</YStack>
)}
</YStack>
)}
{metric === 'constitutional' && (
<YStack>
{result.constitutional.criticalViolations &&
result.constitutional.criticalViolations.length > 0 ? (
<YStack gap="$2">
<Text fontSize={14} fontWeight="500" color="$destructive">
Critical Violations
</Text>
{result.constitutional.criticalViolations.map((v, i) => (
<XStack key={i} alignItems="flex-start" gap="$2">
<X size={16} />
<Text fontSize={14} color="$color11" flex={1}>
{typeof v === 'string' ? v : JSON.stringify(v)}
</Text>
</XStack>
))}
</YStack>
) : (
<YStack gap="$2" alignItems="flex-start">
<Text fontSize={14} fontWeight="500">
Compliance Status
</Text>
<Badge
variant={
result.constitutional.complianceStatus === 'compliant'
? 'default'
: result.constitutional.complianceStatus === 'minor_issues'
? 'secondary'
: 'destructive'
}
>
{result.constitutional.complianceStatus.replace(/_/g, ' ')}
</Badge>
<Text fontSize={14} color="$color11">
All constitutional principles have been evaluated.{' '}
{result.constitutional.principles.filter((p) => p.passed).length} of{' '}
{result.constitutional.principles.length} principles passed.
</Text>
</YStack>
)}
</YStack>
)}
</ScrollView>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}