39246-vm/frontend/components/keywords/ScoringRulesPanel.tsx
abbashkyt-creator 7d8ce0e322 V0.1
2026-03-14 04:02:22 +03:00

206 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import type { ScoringRule } from '@/lib/types'
import { fetchScoringRules, createScoringRule, updateScoringRule, deleteScoringRule } from '@/lib/api/scoring-rules'
import { fetchConfig, saveConfig } from '@/lib/api/config'
import { cn } from '@/lib/utils'
export default function ScoringRulesPanel() {
const qc = useQueryClient()
const { data: rules = [], isLoading } = useQuery<ScoringRule[]>({
queryKey: ['scoring-rules'],
queryFn: fetchScoringRules,
})
const [scoringEnabled, setScoringEnabled] = useState(true)
const [toggleLoading, setToggleLoading] = useState(false)
useEffect(() => {
fetchConfig().then(cfg => {
setScoringEnabled((cfg['scoring_enabled'] ?? 'true') !== 'false')
})
}, [])
const handleScoringToggle = async () => {
setToggleLoading(true)
const newVal = !scoringEnabled
await saveConfig({ scoring_enabled: String(newVal) })
setScoringEnabled(newVal)
setToggleLoading(false)
}
const [newSignal, setNewSignal] = useState('')
const [newDelta, setNewDelta] = useState('')
const [newNotes, setNewNotes] = useState('')
const [error, setError] = useState('')
const [editId, setEditId] = useState<number | null>(null)
const [editSignal, setEditSignal] = useState('')
const [editDelta, setEditDelta] = useState('')
const invalidate = () => qc.invalidateQueries({ queryKey: ['scoring-rules'] })
const createMut = useMutation({
mutationFn: () => createScoringRule(newSignal.trim(), parseInt(newDelta), newNotes.trim() || undefined),
onSuccess: () => { setNewSignal(''); setNewDelta(''); setNewNotes(''); setError(''); invalidate() },
onError: (e: Error) => setError(e.message),
})
const updateMut = useMutation({
mutationFn: () => updateScoringRule(editId!, { signal: editSignal.trim(), delta: parseInt(editDelta) }),
onSuccess: () => { setEditId(null); invalidate() },
})
const deleteMut = useMutation({
mutationFn: (id: number) => deleteScoringRule(id),
onSuccess: invalidate,
})
const positives = rules.filter(r => r.delta > 0)
const negatives = rules.filter(r => r.delta < 0)
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-sm font-semibold text-g-text">Scoring Rules</h2>
<p className="text-xs text-g-faint mt-0.5">Token signals that boost (+) or penalise () lot scores</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className="text-xs text-g-faint">Scoring</span>
<button
onClick={handleScoringToggle}
disabled={toggleLoading}
className={cn(
'g-btn text-xs h-7',
scoringEnabled && '!border-g-green/40 !text-g-green !bg-g-green/8'
)}
>
{scoringEnabled ? 'On' : 'Off'}
</button>
</div>
</div>
{/* AI-first banner */}
{!scoringEnabled && (
<div className="g-card border-g-green/20 bg-g-green/4 px-4 py-3 text-xs text-g-green space-y-1">
<div className="font-semibold">AI-first mode active</div>
<div className="text-g-muted leading-relaxed">
Score signals are disabled. The AI description on each target is the sole judge.
Set an AI Description on every target and enable AI Filter in Settings.
</div>
</div>
)}
<div className={cn(scoringEnabled ? '' : 'opacity-40 pointer-events-none select-none', 'space-y-4')}>
{/* Add new */}
<form
className="g-card p-4 flex gap-2 flex-wrap items-end"
onSubmit={e => { e.preventDefault(); if (newSignal && newDelta) createMut.mutate() }}
>
<div className="space-y-1">
<label className="text-[10px] uppercase tracking-widest text-g-faint">Signal</label>
<input className="g-input h-8 w-32 text-sm" placeholder="RTX" value={newSignal} onChange={e => setNewSignal(e.target.value)} />
</div>
<div className="space-y-1">
<label className="text-[10px] uppercase tracking-widest text-g-faint">Delta</label>
<input type="number" className="g-input h-8 w-20 text-sm font-mono" placeholder="+10" value={newDelta} onChange={e => setNewDelta(e.target.value)} />
</div>
<div className="space-y-1 flex-1 min-w-36">
<label className="text-[10px] uppercase tracking-widest text-g-faint">Notes</label>
<input className="g-input h-8 text-sm" placeholder="GPU keyword" value={newNotes} onChange={e => setNewNotes(e.target.value)} />
</div>
<button
type="submit"
disabled={!newSignal || !newDelta || createMut.isPending}
className="g-btn-primary h-8 text-xs disabled:opacity-40"
>
+ Add rule
</button>
{error && <span className="text-xs text-g-red w-full">{error}</span>}
</form>
{isLoading && <p className="text-xs text-g-faint">Loading</p>}
{/* Rules tables */}
{positives.length > 0 && (
<div className="g-card overflow-hidden">
<div className="px-4 py-2.5 border-b border-g-border/50 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-g-green" />
<span className="text-xs font-medium text-g-green uppercase tracking-wider">Boosts</span>
</div>
<table className="g-table">
<thead><tr><th>Signal</th><th>Delta</th><th>Notes</th><th className="w-10"></th></tr></thead>
<tbody>
{positives.map(r => (
<RuleRow key={r.id} rule={r} editId={editId} editSignal={editSignal} editDelta={editDelta}
setEditId={setEditId} setEditSignal={setEditSignal} setEditDelta={setEditDelta}
onSave={() => updateMut.mutate()} onDelete={() => deleteMut.mutate(r.id)} />
))}
</tbody>
</table>
</div>
)}
{negatives.length > 0 && (
<div className="g-card overflow-hidden">
<div className="px-4 py-2.5 border-b border-g-border/50 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-g-red" />
<span className="text-xs font-medium text-g-red uppercase tracking-wider">Penalties</span>
</div>
<table className="g-table">
<thead><tr><th>Signal</th><th>Delta</th><th>Notes</th><th className="w-10"></th></tr></thead>
<tbody>
{negatives.map(r => (
<RuleRow key={r.id} rule={r} editId={editId} editSignal={editSignal} editDelta={editDelta}
setEditId={setEditId} setEditSignal={setEditSignal} setEditDelta={setEditDelta}
onSave={() => updateMut.mutate()} onDelete={() => deleteMut.mutate(r.id)} />
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)
}
function RuleRow({ rule, editId, editSignal, editDelta, setEditId, setEditSignal, setEditDelta, onSave, onDelete }: {
rule: ScoringRule; editId: number | null
editSignal: string; editDelta: string
setEditId: (id: number | null) => void; setEditSignal: (s: string) => void; setEditDelta: (s: string) => void
onSave: () => void; onDelete: () => void
}) {
const isEditing = editId === rule.id
if (isEditing) return (
<tr>
<td><input className="g-input h-7 text-sm w-28" value={editSignal} onChange={e => setEditSignal(e.target.value)} /></td>
<td><input type="number" className="g-input h-7 text-sm font-mono w-16" value={editDelta} onChange={e => setEditDelta(e.target.value)} /></td>
<td className="text-xs text-g-faint">{rule.notes || '—'}</td>
<td className="flex gap-2 items-center">
<button onClick={onSave} className="g-btn text-xs h-7">Save</button>
<button onClick={() => setEditId(null)} className="text-g-faint hover:text-g-text text-xs">×</button>
</td>
</tr>
)
return (
<tr className="group">
<td>
<button className="text-sm text-g-text hover:text-g-green transition-colors font-medium"
onClick={() => { setEditId(rule.id); setEditSignal(rule.signal); setEditDelta(String(rule.delta)) }}>
{rule.signal}
</button>
</td>
<td>
<span className={cn('font-mono text-sm font-bold', rule.delta > 0 ? 'text-g-green' : 'text-g-red')}>
{rule.delta > 0 ? '+' : ''}{rule.delta}
</span>
</td>
<td className="text-xs text-g-faint">{rule.notes || '—'}</td>
<td>
<button onClick={onDelete}
className="text-g-faint hover:text-g-red transition-colors text-xs opacity-0 group-hover:opacity-100"></button>
</td>
</tr>
)
}