"use client";
/**
* `` — admin view onto the v1.4.23 H7 Coach
* helpful/unhelpful aggregate. Reads the same feedback-aggregator row
* that the AI Quality view consumes; this section just slices the
* Coach-only buckets so the operator can see prompt-version × helpful-
* rate × n at a glance.
*
* First useful question this answers in the v1.4.23 first-week window:
* "Is the v1.4.22 Coach prose rewrite (PROMPT_VERSION 4.22.0 → 4.23.x)
* landing well, or did the warm tone overshoot?" If the helpful-rate
* for the new prompt version drops below 50 % within the first 100
* ratings, v1.4.24 walks the persona back.
*
* Empty state when the aggregator hasn't run or no Coach feedback has
* landed in the rolling window. Cross-user aggregation only — the
* admin view never exposes per-user prose.
*/
import { useQuery } from "@tanstack/react-query";
import { Loader2, Sparkles } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
import { helpfulRateColour } from "./_shared";
interface CoachFeedbackBucket {
promptVersion: string;
tone: string;
verbosity: string;
helpful: number;
notHelpful: number;
total: number;
helpfulRate: number;
}
interface FeedbackSummary {
generatedAt: string;
windowDays: number;
coachBuckets?: CoachFeedbackBucket[];
}
export function CoachFeedbackSection() {
const { t } = useTranslations();
const fmt = useFormatters();
const query = useQuery({
queryKey: queryKeys.adminCoachFeedback(),
queryFn: async () => {
const data = await apiGet<{ summary: FeedbackSummary | null } | null>(
"/api/admin/ai-quality",
);
return data?.summary ?? null;
},
});
const summary = query.data ?? null;
const buckets = summary?.coachBuckets ?? [];
const hasData = !!summary && buckets.length > 0;
// v1.4.25 W8 — keep the section's outer card + header rendered across
// every query state (loading / error / empty / data) so the heading
// baseline stays at a constant Y-offset. Previously the loading branch
// returned a thin flex-row stub and the error branch a p-4 alert with
// NO heading, which made the entire section snap downward when the
// fetched data resolved — the maintainer reported the visible layout shift on
// /admin/coach-feedback. Mirrors the canonical structure used by
// (header outside the fetch-state branch).
return (
{t("admin.coachFeedback.windowLabel", {
days: String(summary.windowDays),
})}
{" · "}
{t("admin.coachFeedback.generatedAtLabel")}{" "}
{fmt.dateTime(summary.generatedAt)}
) : null
}
/>
{query.isLoading ? (
{t("admin.coachFeedback.loading")}
) : query.isError ? (
{t("admin.coachFeedback.loadError")}
) : !hasData ? (
{t("admin.coachFeedback.empty")}
) : (
|
{t("admin.coachFeedback.colPromptVersion")}
|
{t("admin.coachFeedback.colTone")}
|
{t("admin.coachFeedback.colVerbosity")}
|
{t("admin.coachFeedback.colHelpful")}
|
{t("admin.coachFeedback.colNotHelpful")}
|
{t("admin.coachFeedback.colN")}
|
{t("admin.coachFeedback.colHelpfulRate")}
|
{buckets.map((bucket, idx) => {
const ratePct = Math.round(bucket.helpfulRate * 100);
return (
|
{bucket.promptVersion}
|
{bucket.tone} |
{bucket.verbosity} |
{bucket.helpful} |
{bucket.notHelpful}
|
{bucket.total} |
{ratePct}%
|
);
})}
)}
);
}