97 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|