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

113 lines
4.2 KiB
TypeScript

'use client'
import { useState, useRef } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useUpdateSite, useDeleteSite, useAdaptSite, useSiteSelectors } from '@/hooks/useSites'
import { useQueryClient } from '@tanstack/react-query'
import type { TargetSite } from '@/lib/types'
import { cn } from '@/lib/utils'
function HealthBadge({ site }: { site: TargetSite }) {
const inCooldown = site.cooldown_until && new Date(site.cooldown_until) > new Date()
if (inCooldown) return <span className="g-badge g-badge-amber">Cooldown</span>
if (site.consecutive_failures > 2) return <span className="g-badge g-badge-red">{site.error_count} errors</span>
return <span className="g-badge g-badge-green">OK</span>
}
function ConfidenceBadge({ siteId }: { siteId: number }) {
const { data: sel } = useSiteSelectors(siteId)
if (!sel) return <span className="text-g-faint text-xs"></span>
const cls = sel.confidence >= 70 ? 'g-badge-green' : sel.confidence >= 40 ? 'g-badge-amber' : 'g-badge-red'
return (
<span className={`g-badge ${cls}`}>
{sel.confidence}%{sel.stale ? ' ⚠' : ''}
</span>
)
}
const ADAPT_POLL_DELAY = 45_000
export default function SiteRow({ site }: { site: TargetSite }) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: site.id })
const updateSiteMut = useUpdateSite()
const deleteSiteMut = useDeleteSite()
const adaptSiteMut = useAdaptSite()
const qc = useQueryClient()
const [adapting, setAdapting] = useState(false)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const handleAdapt = () => {
adaptSiteMut.mutate(site.id, {
onSuccess: () => {
setAdapting(true)
if (timerRef.current) clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
setAdapting(false)
qc.invalidateQueries({ queryKey: ['selectors', site.id] })
}, ADAPT_POLL_DELAY)
},
})
}
const style = { transform: CSS.Transform.toString(transform), transition }
const isAdapting = adaptSiteMut.isPending || adapting
return (
<tr ref={setNodeRef} style={style} className="group">
<td className="w-8">
<span
{...attributes} {...listeners}
className="cursor-grab text-g-faint/30 hover:text-g-faint transition-colors select-none"
>
</span>
</td>
<td>
<span className="text-sm font-medium text-g-text">{site.name}</span>
</td>
<td>
<span className="text-xs font-mono text-g-faint truncate max-w-xs block">{site.url_template}</span>
</td>
<td><HealthBadge site={site} /></td>
<td><ConfidenceBadge siteId={site.id} /></td>
<td>
<label className="flex items-center gap-2 cursor-pointer w-fit">
<div
onClick={() => updateSiteMut.mutate({ id: site.id, data: { enabled: site.enabled ? 0 : 1 } })}
className={cn(
'relative w-8 h-4 rounded-full transition-colors duration-200 cursor-pointer flex-shrink-0',
site.enabled ? 'bg-g-green/30 border border-g-green/40' : 'bg-g-raised border border-g-border'
)}
>
<span className={cn(
'absolute top-0.5 w-3 h-3 rounded-full transition-transform duration-200 shadow-sm',
site.enabled ? 'left-[18px] bg-g-green' : 'left-0.5 bg-g-faint'
)} />
</div>
<span className="text-xs text-g-faint">{site.enabled ? 'On' : 'Off'}</span>
</label>
</td>
<td>
<div className="flex items-center gap-2">
<button
onClick={handleAdapt}
disabled={isAdapting}
className={cn(
'g-btn text-xs h-7',
isAdapting && 'opacity-60 cursor-not-allowed'
)}
>
{isAdapting ? 'Adapting…' : 'Adapt AI'}
</button>
<button
onClick={() => { if (confirm(`Delete "${site.name}"?`)) deleteSiteMut.mutate(site.id) }}
className="text-g-faint hover:text-g-red transition-colors text-xs opacity-0 group-hover:opacity-100"
>
</button>
</div>
</td>
</tr>
)
}