feat: enhance ai quality improvement operations
This commit is contained in:
+89
-2
@@ -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";
|
||||
|
||||
+5
-3
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user