9ad4343c76
The verify half of the tune→verify loop (#1251), on the same admin Tuning page as the knobs: - RecommendationWeeklyTrends: weekly per-source outcomes aggregated across all users (the knobs are global, so judging a turn needs global outcomes — rows carry rates only, no track/user identity), with a taste-hit count per bucket: plays whose track's artist has a positive weight in the player's current taste profile. That's the "cheap recompute" reading — retroactive over the whole window, at the cost of profile drift. - GET /api/admin/recommendation-trends?weeks=N (default 12, cap 52): per-family weekly series (skip rate, sample-weighted completion, taste-hit rate) plus the tuning-audit markers inside the window. - Web: sparkline table under the tuning cards — skip rate per week on a shared axis with dashed ticks at knob turns, latest-week columns, window taste-hit rate, low-volume rows dimmed as anecdote, and a plain-text list of the window's tuning changes. Also fixes the revive unused-parameter lint on the tuning GET handler that failed CI run 1903 on the previous commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
87 lines
2.1 KiB
TypeScript
87 lines
2.1 KiB
TypeScript
import { api } from './client';
|
|
|
|
// Mirrors internal/api/admin_recommendation_tuning.go (#1250): the
|
|
// recommendation tuning lab. Field names are the server's snake_case
|
|
// keys, used verbatim in PATCH bodies.
|
|
|
|
export type WeightProfile = {
|
|
base_weight: number;
|
|
like_boost: number;
|
|
recency_weight: number;
|
|
skip_penalty: number;
|
|
jitter_magnitude: number;
|
|
context_weight: number;
|
|
similarity_weight: number;
|
|
taste_weight: number;
|
|
};
|
|
|
|
export type TasteTuning = {
|
|
half_life_days: number;
|
|
engagement_hard_skip: number;
|
|
engagement_neutral: number;
|
|
engagement_full: number;
|
|
};
|
|
|
|
export type TuningScope = 'radio' | 'daily_mix' | 'taste';
|
|
|
|
export type TuningSnapshot = {
|
|
profiles: Record<'radio' | 'daily_mix', WeightProfile>;
|
|
taste: TasteTuning;
|
|
shipped: {
|
|
profiles: Record<'radio' | 'daily_mix', WeightProfile>;
|
|
taste: TasteTuning;
|
|
};
|
|
};
|
|
|
|
export function getTuning(): Promise<TuningSnapshot> {
|
|
return api.get<TuningSnapshot>('/api/admin/recommendation-tuning');
|
|
}
|
|
|
|
export function patchTuning(
|
|
scope: TuningScope,
|
|
values: Record<string, number>
|
|
): Promise<TuningSnapshot> {
|
|
return api.patch<TuningSnapshot>(`/api/admin/recommendation-tuning/${scope}`, { values });
|
|
}
|
|
|
|
export function resetTuning(scope: TuningScope): Promise<TuningSnapshot> {
|
|
return api.post<TuningSnapshot>(`/api/admin/recommendation-tuning/${scope}/reset`, {});
|
|
}
|
|
|
|
// Trends (#1251): weekly per-surface outcome series with knob-turn
|
|
// markers. Mirrors internal/api/admin_recommendation_trends.go.
|
|
|
|
export type TrendPoint = {
|
|
week_start: string;
|
|
plays: number;
|
|
skips: number;
|
|
skip_rate: number;
|
|
avg_completion: number;
|
|
taste_hit_rate: number;
|
|
};
|
|
|
|
export type TrendSeries = {
|
|
key: string;
|
|
label: string;
|
|
intent: string;
|
|
plays: number;
|
|
points: TrendPoint[];
|
|
};
|
|
|
|
export type TrendMarker = {
|
|
changed_at: string;
|
|
scope: string;
|
|
action: string;
|
|
changes: { field: string; old: number; new: number }[];
|
|
};
|
|
|
|
export type TrendsResponse = {
|
|
weeks: number;
|
|
series: TrendSeries[];
|
|
markers: TrendMarker[];
|
|
};
|
|
|
|
export function getTrends(weeks = 12): Promise<TrendsResponse> {
|
|
return api.get<TrendsResponse>(`/api/admin/recommendation-trends?weeks=${weeks}`);
|
|
}
|