assistant/console/components/EntryCard.tsx

97 lines
4.2 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Check, Sparkles, Undo2, X } from 'lucide-react';
import { CATEGORY_LABEL, SCOPE_LABEL, STATUS_LABEL, SUGGESTION_LABEL, type Entry, type Scope } from '../types';
export function EntryCard({ entry, selected, busy, onToggle, onReview, onSuggest }: {
entry: Entry;
selected: boolean;
busy: boolean;
onToggle: () => void;
onReview: (action: 'verify' | 'reject' | 'reset', value?: string, scope?: Scope) => void;
onSuggest: () => void;
}) {
const [editing, setEditing] = useState(false);
const [value, setValue] = useState(entry.value);
const [scope, setScope] = useState<Scope>(entry.scope);
useEffect(() => {
setValue(entry.value);
setScope(entry.scope);
}, [entry.id, entry.value, entry.scope]);
const schools = (entry.schools ?? '').split(',').filter(Boolean);
const changed = value.trim() !== entry.value.trim();
return (
<article className={`entry${selected ? ' selected' : ''}`} data-status={entry.status}>
<header className="entry-head">
<input type="checkbox" checked={selected} onChange={onToggle} aria-label={`选中 ${entry.key}`} />
<strong className="entry-key">{entry.key}</strong>
<span className={`chip cat-${entry.category}`}>{CATEGORY_LABEL[entry.category] ?? entry.category}</span>
<span className={`chip status-${entry.status}`}>{STATUS_LABEL[entry.status]}</span>
{entry.suggestion && (
<span className={`chip sug-${entry.suggestion}`} title={entry.suggestion_note}>
{SUGGESTION_LABEL[entry.suggestion]}
</span>
)}
<span className="spacer" />
{(entry.confirmed > 0 || entry.contradicted > 0) && (
<span className={`chip ${entry.contradicted > 0 ? 'sug-reject' : 'sug-adopt'}`}
title="Agent 在真实页面上验证的结果,比观察次数硬">
实测 {entry.confirmed} 相符{entry.contradicted > 0 ? ` / ${entry.contradicted} 不符` : ''}
</span>
)}
<span className="entry-meta">
{entry.school_count} 校 · 观察 {entry.observations}
{entry.status !== 'draft' && ` · ${SCOPE_LABEL[entry.scope]}`}
</span>
</header>
{editing ? (
<textarea className="entry-edit" value={value} rows={4} onChange={event => setValue(event.target.value)} />
) : (
<p className="entry-value">{entry.value}</p>
)}
{entry.suggestion_note && <p className="entry-note">{entry.suggestion_note}</p>}
<footer className="entry-foot">
<span className="entry-source">
{entry.knowledge_id} / {entry.business_id || '全局'}
{schools.length > 0 && ` · ${schools.join('、')}`}
</span>
<span className="spacer" />
{entry.status === 'draft' ? (
<>
<label className="scope-pick">
发布给
<select value={scope} onChange={event => setScope(event.target.value as Scope)}>
<option value="origin">仅来源学校</option>
<option value="all">所有学校</option>
</select>
</label>
<button type="button" className="text-action" disabled={busy} onClick={onSuggest} title="让模型先给个建议">
<Sparkles size={14} />预审
</button>
<button type="button" className="text-action" disabled={busy} onClick={() => setEditing(v => !v)}>
{editing ? '取消改写' : '改写'}
</button>
<button type="button" className="danger-button small" disabled={busy}
onClick={() => onReview('reject')}>
<X size={14} />驳回
</button>
<button type="button" className="primary-button small" disabled={busy}
onClick={() => onReview('verify', changed ? value : undefined, scope)}>
<Check size={14} />{changed ? '改写并采纳' : '采纳'}
</button>
</>
) : (
<button type="button" className="text-action" disabled={busy} onClick={() => onReview('reset')}>
<Undo2 size={14} />退回待审
</button>
)}
</footer>
</article>
);
}