"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")}

) : (
{buckets.map((bucket, idx) => { const ratePct = Math.round(bucket.helpfulRate * 100); return ( ); })}
{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")}
{bucket.promptVersion} {bucket.tone} {bucket.verbosity} {bucket.helpful} {bucket.notHelpful} {bucket.total} {ratePct}%
)}
); }