feat: enhance ai quality improvement operations

This commit is contained in:
2026-06-23 08:38:51 +08:00
parent 87bb3c2b75
commit f145a38ee5
10 changed files with 337 additions and 16 deletions
@@ -96,6 +96,8 @@ type QualityImprovementAction = {
affected_rate_delta: number;
pending_labels_delta: number;
quality_score_delta: number;
effect_status?: string;
effect_summary?: string | null;
resolution_note?: string | null;
created_at: string;
updated_at: string;
@@ -115,6 +117,14 @@ type LoadState =
| { status: "ready"; dashboard: QualityDashboard }
| { status: "error"; message: string };
type ActionFilters = {
status: string;
owner: string;
priority: string;
actionType: string;
dimension: string;
};
const severityTone: Record<string, string> = {
info: "tag-primary",
warning: "tag-warning",
@@ -152,11 +162,32 @@ const dimensionLabels: Record<string, string> = {
label_code: "标签"
};
const effectTone: Record<string, string> = {
improved: "tag-success",
unchanged: "tag-warning",
regressed: "tag-danger",
pending_data: ""
};
const effectLabels: Record<string, string> = {
improved: "已改善",
unchanged: "未明显变化",
regressed: "有恶化",
pending_data: "待观察"
};
export default function AiQualityDashboardPage() {
const [state, setState] = useState<LoadState>({ status: "loading" });
const [days, setDays] = useState(14);
const [actionMessage, setActionMessage] = useState<string | null>(null);
const [actionBusyKey, setActionBusyKey] = useState<string | null>(null);
const [filters, setFilters] = useState<ActionFilters>({
status: "",
owner: "",
priority: "",
actionType: "",
dimension: ""
});
async function loadDashboard(nextDays = days) {
setState({ status: "loading" });
@@ -185,7 +216,9 @@ export default function AiQualityDashboardPage() {
const dashboard = state.status === "ready" ? state.dashboard : null;
const improvementActions = dashboard?.improvement_actions ?? [];
const filteredActions = filterImprovementActions(improvementActions, filters);
const openActionCount = improvementActions.filter((action) => ["open", "in_progress"].includes(action.status)).length;
const ownerOptions = Array.from(new Set(improvementActions.map((action) => action.owner).filter(Boolean))).sort() as string[];
function changeDays(value: string) {
const nextDays = Number(value);
@@ -194,6 +227,10 @@ export default function AiQualityDashboardPage() {
void loadDashboard(nextDays);
}
function updateFilter(key: keyof ActionFilters, value: string) {
setFilters((current) => ({ ...current, [key]: value }));
}
async function createActionFromAttribution(item: QualityAttributionItem) {
const busyKey = attributionBusyKey(item);
setActionBusyKey(busyKey);
@@ -331,11 +368,46 @@ export default function AiQualityDashboardPage() {
<span className="tag tag-primary">待推进 {openActionCount}</span>
</div>
<div className="panel-body">
<div className="quality-action-filters">
<select className="search-input select-input-compact" value={filters.status} onChange={(event) => updateFilter("status", event.target.value)}>
<option value="">全部状态</option>
{Object.entries(statusLabels).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
<select className="search-input select-input-compact" value={filters.owner} onChange={(event) => updateFilter("owner", event.target.value)}>
<option value="">全部负责人</option>
{ownerOptions.map((owner) => (
<option key={owner} value={owner}>{owner}</option>
))}
</select>
<select className="search-input select-input-compact" value={filters.priority} onChange={(event) => updateFilter("priority", event.target.value)}>
<option value="">全部优先级</option>
<option value="critical">critical</option>
<option value="high">high</option>
<option value="medium">medium</option>
<option value="low">low</option>
</select>
<select className="search-input select-input-compact" value={filters.actionType} onChange={(event) => updateFilter("actionType", event.target.value)}>
<option value="">全部动作</option>
{Object.entries(actionTypeLabels).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
<select className="search-input select-input-compact" value={filters.dimension} onChange={(event) => updateFilter("dimension", event.target.value)}>
<option value="">全部维度</option>
{Object.entries(dimensionLabels).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</div>
{improvementActions.length === 0 ? (
<div className="empty-state empty-state-compact">暂无改进项,可先从上方归因卡片生成。</div>
) : filteredActions.length === 0 ? (
<div className="empty-state empty-state-compact">当前筛选条件下没有改进项。</div>
) : (
<div className="quality-action-list">
{improvementActions.map((action) => (
{filteredActions.map((action) => (
<QualityActionItem
action={action}
busyKey={actionBusyKey}
@@ -499,6 +571,7 @@ function QualityActionItem({
const busy = busyKey?.startsWith(`${action.id}:`) ?? false;
const baselineRate = safeRate(action.baseline_affected_runs, action.baseline_total_runs);
const currentRate = safeRate(action.current_affected_runs, action.current_total_runs);
const effectStatus = action.effect_status ?? "pending_data";
return (
<div className="quality-action-item">
@@ -512,7 +585,10 @@ function QualityActionItem({
<span>{action.owner || "未分配"}</span>
</div>
</div>
<span className={`tag ${statusTone[action.status] ?? ""}`}>{statusLabels[action.status] ?? action.status}</span>
<div className="action-row">
<span className={`tag ${effectTone[effectStatus] ?? ""}`}>{effectLabels[effectStatus] ?? effectStatus}</span>
<span className={`tag ${statusTone[action.status] ?? ""}`}>{statusLabels[action.status] ?? action.status}</span>
</div>
</div>
{(action.description || action.root_cause_hint) && (
<span className="item-meta">{action.description || action.root_cause_hint}</span>
@@ -524,6 +600,7 @@ function QualityActionItem({
<span>变化 {formatSignedPercent(action.affected_rate_delta)}</span>
</div>
{action.resolution_note && <span className="item-meta">处理说明:{action.resolution_note}</span>}
{action.effect_summary && <span className="item-meta">效果复盘:{action.effect_summary}</span>}
<div className="action-row">
{action.status === "open" && (
<button className="button button-soft" disabled={busy} onClick={() => void onUpdate(action, "in_progress")} type="button">
@@ -548,6 +625,16 @@ function QualityActionItem({
);
}
function filterImprovementActions(actions: QualityImprovementAction[], filters: ActionFilters) {
return actions.filter((action) => (
(!filters.status || action.status === filters.status)
&& (!filters.owner || action.owner === filters.owner)
&& (!filters.priority || action.priority === filters.priority)
&& (!filters.actionType || action.action_type === filters.actionType)
&& (!filters.dimension || action.dimension === filters.dimension)
));
}
function inferActionType(item: QualityAttributionItem) {
if (item.dimension === "prompt_template") {
return "tune_prompt";
@@ -7,12 +7,14 @@ const aiPlatformBaseUrl =
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const status = searchParams.get("status");
const limit = searchParams.get("limit") ?? "20";
const targetUrl = new URL("/api/v1/audit/quality-improvement-actions", aiPlatformBaseUrl);
targetUrl.searchParams.set("limit", limit);
if (status) {
targetUrl.searchParams.set("status", status);
for (const name of ["status", "owner", "priority", "action_type", "dimension"]) {
const value = searchParams.get(name);
if (value) {
targetUrl.searchParams.set(name, value);
}
}
try {
@@ -743,6 +743,17 @@ select {
gap: 12px;
}
.quality-action-filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.quality-action-filters .search-input {
min-width: 128px;
}
.quality-action-item {
display: grid;
gap: 10px;