abbashkyt-creator 7d8ce0e322 V0.1
2026-03-14 04:02:22 +03:00

124 lines
4.3 KiB
TypeScript

'use client'
import { useState } from 'react'
import { cn } from '@/lib/utils'
interface RawEntry {
id: number
ts: string
call_type: string
direction: string
provider?: string
model?: string
content?: string
title?: string
site?: string
tokens_prompt?: number
tokens_completion?: number
verdict?: string
status_code?: number
}
export default function AILogCard({ entry }: { entry: RawEntry }) {
const [expanded, setExpanded] = useState(false)
const isRequest = entry.direction === 'request'
const isResponse = entry.direction === 'response'
const isError = entry.direction === 'error'
const isMatch = entry.verdict === 'YES'
const isReject = entry.verdict === 'NO'
const totalTokens = (entry.tokens_prompt ?? 0) + (entry.tokens_completion ?? 0)
return (
<div className={cn(
'rounded-lg border text-sm transition-colors',
isError ? 'border-g-red/20 bg-g-red/4' :
isMatch ? 'border-g-green/20 bg-g-green/4' :
isReject ? 'border-g-line' :
'border-g-border/60',
)}>
{/* Header */}
<div className="flex items-center justify-between gap-3 flex-wrap px-3.5 py-2.5">
<div className="flex items-center gap-2 flex-wrap">
{/* Direction badge */}
<span className={cn(
'g-badge',
isRequest ? 'g-badge-neutral' :
isResponse ? (isMatch ? 'g-badge-green' : isReject ? 'g-badge-red' : 'g-badge-neutral') :
'g-badge-red'
)}>
{isRequest ? '→ Prompt' : isResponse ? '← Response' : '⚠ Error'}
</span>
<span className="g-badge g-badge-neutral">{entry.call_type}</span>
{entry.provider && <span className="text-xs text-g-faint">{entry.provider}</span>}
{entry.model && (
<span className="text-xs text-g-faint truncate max-w-[160px]">{entry.model}</span>
)}
</div>
<span className="text-[11px] text-g-faint tabular-nums shrink-0">
{new Date(entry.ts).toLocaleTimeString()}
</span>
</div>
{/* Lot info */}
{(entry.title || entry.site) && (
<div className="px-3.5 pb-2 flex items-center gap-2 flex-wrap">
{entry.title && (
<span className="text-xs text-g-text leading-relaxed">
{entry.title.length > 72 ? entry.title.slice(0, 72) + '…' : entry.title}
</span>
)}
{entry.site && <span className="text-xs text-g-faint">· {entry.site}</span>}
</div>
)}
{/* Verdict */}
{isResponse && entry.verdict && (
<div className={cn(
'px-3.5 pb-2 flex items-center gap-1.5 text-xs font-medium',
isMatch ? 'text-g-green' : 'text-g-red'
)}>
<span className={cn('w-1.5 h-1.5 rounded-full flex-shrink-0', isMatch ? 'bg-g-green' : 'bg-g-red')} />
{isMatch ? 'Match — lot accepted' : 'Reject — lot filtered out'}
</div>
)}
{/* Expandable content */}
{entry.content && (
<div className="px-3.5 pb-3">
<pre className={cn(
'whitespace-pre-wrap break-words text-[11px] leading-relaxed font-mono',
'bg-g-base/80 border border-g-border/40 p-3 rounded-md',
isRequest ? 'text-g-faint' : 'text-g-muted',
!expanded && 'max-h-20 overflow-hidden',
)}>
{entry.content}
</pre>
{entry.content.length > 180 && (
<button
onClick={() => setExpanded(!expanded)}
className="text-g-faint hover:text-g-muted text-[11px] mt-1 transition-colors"
>
{expanded ? '▲ Collapse' : `▼ Expand (${entry.content.length} chars)`}
</button>
)}
</div>
)}
{/* Token counts */}
{(entry.tokens_prompt != null || entry.tokens_completion != null) && (
<div className="px-3.5 pb-2.5 flex items-center gap-2 text-[11px] text-g-faint">
<span className="g-badge g-badge-neutral">
{entry.tokens_prompt ?? '?'} + {entry.tokens_completion ?? '?'} = {totalTokens} tokens
</span>
</div>
)}
{/* Error */}
{isError && entry.status_code && (
<div className="px-3.5 pb-2 text-xs text-g-red font-mono">HTTP {entry.status_code}</div>
)}
</div>
)
}