style: enlarge leaderboard report dialog
This commit is contained in:
1 parent
d59f9e218f
commit
0d91c74289
1 file changed
+98
-101
@@ -25,7 +25,7 @@ import {
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -521,9 +521,87 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP
|
||||
const displayedSeries = metric === "all" ? series : [activeSeries];
|
||||
const totalFor = (key: TrendMetric) => visible.reduce((sum, point) => sum + Number(point[key] ?? 0), 0);
|
||||
const activeDays = visible.filter((point) => point.runs > 0 || point.questions > 0 || point.tokens > 0).length;
|
||||
const metricCards = ({ includeAll = true, large = false }: { includeAll?: boolean; large?: boolean } = {}) => (
|
||||
<div className={cn("grid gap-2", includeAll ? "sm:grid-cols-2 xl:grid-cols-5" : "sm:grid-cols-4")}>
|
||||
{includeAll && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMetric("all")}
|
||||
className={cn(
|
||||
"min-w-0 rounded-lg border px-3 py-2 text-left transition hover:-translate-y-0.5 hover:shadow-sm",
|
||||
large && "px-4 py-3",
|
||||
metric === "all"
|
||||
? "border-[#C1121F] bg-[#FFF1F1] shadow-sm dark:border-[#F2B8BB]/40 dark:bg-[#3A1215]"
|
||||
: "border-[#F1D6D6] bg-white dark:border-[#462123] dark:bg-[#230D0F]",
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex rounded-full bg-[#FFF1F1] px-2 py-0.5 text-xs font-medium text-[#C1121F]">全部趋势</span>
|
||||
<span className={cn("mt-2 block font-semibold tabular-nums text-[#231815] dark:text-white", large ? "text-2xl" : "text-lg")}>
|
||||
{fmtNumber(visible.reduce((sum, point) => sum + point.runs + point.questions + point.users, 0))}
|
||||
</span>
|
||||
{large && <span className="mt-1 block truncate text-xs text-[#8B6B6B] dark:text-[#D8B8B8]">同时显示运行、问答、活跃与 Token</span>}
|
||||
</button>
|
||||
)}
|
||||
{series.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setMetric(item.key)}
|
||||
className={cn(
|
||||
"min-w-0 rounded-lg border px-3 py-2 text-left transition hover:-translate-y-0.5 hover:shadow-sm",
|
||||
large && "px-4 py-3",
|
||||
metric === item.key
|
||||
? "border-[#C1121F] bg-[#FFF1F1] shadow-sm dark:border-[#F2B8BB]/40 dark:bg-[#3A1215]"
|
||||
: "border-[#F1D6D6] bg-white dark:border-[#462123] dark:bg-[#230D0F]",
|
||||
)}
|
||||
title={item.description}
|
||||
>
|
||||
<span className={cn("inline-flex rounded-full px-2 py-0.5 text-xs font-medium", item.soft)}>{item.label}</span>
|
||||
<span className={cn("mt-2 block truncate font-semibold tabular-nums text-[#111827] dark:text-white", large ? "text-2xl" : compact ? "text-base" : "text-lg")}>
|
||||
{fmtNumber(totalFor(item.key))}
|
||||
</span>
|
||||
{(large || compact) && <span className="mt-1 block truncate text-[11px] text-[#8B6B6B] dark:text-[#D8B8B8]">{item.description}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const analysisDialog = (title: string, description: ReactNode) => (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className={compact ? "h-8" : undefined}>
|
||||
<Maximize2 className="mr-1 size-4" />
|
||||
{compact ? "放大" : "放大分析"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="flex h-[92vh] w-[96vw] max-w-none flex-col gap-4 overflow-hidden p-5 sm:max-w-none">
|
||||
<DialogHeader className="shrink-0 pr-10">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="shrink-0">{metricCards({ includeAll: true, large: true })}</div>
|
||||
<div className="grid min-h-0 flex-1 gap-3 lg:grid-cols-[260px_minmax(0,1fr)]">
|
||||
<div className="rounded-lg border border-[#F1D6D6] bg-white p-4 dark:border-[#462123] dark:bg-[#230D0F]">
|
||||
<div className="text-xs text-[#7A5A5A] dark:text-[#D8B8B8]">当前查看</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-[#111827] dark:text-white">{metric === "all" ? "全部指标" : activeSeries.label}</div>
|
||||
<div className="mt-5 text-xs text-[#7A5A5A] dark:text-[#D8B8B8]">最新 {metric === "all" ? "运行" : activeSeries.label}</div>
|
||||
<div className="mt-2 text-4xl font-semibold tabular-nums text-[#111827] dark:text-white">{fmtNumber(latestValue)}</div>
|
||||
<div className={cn("mt-3 text-sm", delta >= 0 ? "text-[#C1121F]" : "text-[#7F1D1D] dark:text-[#FCA5A5]")}>
|
||||
较上一日 {delta >= 0 ? "+" : ""}{fmtNumber(delta)}
|
||||
</div>
|
||||
<div className="mt-5 text-xs text-[#7A5A5A] dark:text-[#D8B8B8]">
|
||||
峰值 {peak.date} / {fmtNumber(Number(peak[activeMetric] ?? 0))}
|
||||
</div>
|
||||
</div>
|
||||
{chart(true)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const chart = (large = false) => (
|
||||
<div className={cn("min-w-0 overflow-hidden rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-3 dark:border-[#3F3F46] dark:bg-[#18181B]", large && "p-5")}>
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className={cn("w-full", large ? "h-[520px]" : compact ? "h-56" : "h-72")} role="img" aria-label="使用趋势折线图">
|
||||
<div className={cn("min-w-0 overflow-hidden rounded-xl border border-[#E5E7EB] bg-[#F9FAFB] p-3 dark:border-[#3F3F46] dark:bg-[#18181B]", large && "flex min-h-0 p-5")}>
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className={cn("w-full", large ? "h-full min-h-[420px]" : compact ? "h-56" : "h-72")} role="img" aria-label="使用趋势折线图">
|
||||
<defs>
|
||||
<linearGradient id={`trend-area-${chartId}-${activeMetric}`} x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor={activeSeries.color} stopOpacity="0.22" />
|
||||
@@ -598,47 +676,13 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="min-w-0 space-y-3">
|
||||
<div className="grid gap-2 sm:grid-cols-4">
|
||||
{series.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setMetric(item.key)}
|
||||
className={cn(
|
||||
"min-w-0 rounded-lg border px-3 py-2 text-left transition hover:border-[#C1121F]",
|
||||
metric === item.key
|
||||
? "border-[#C1121F] bg-[#FFF1F1] dark:bg-[#3A1215]"
|
||||
: "border-[#F1D6D6] bg-white dark:border-[#462123] dark:bg-[#230D0F]",
|
||||
)}
|
||||
title={item.description}
|
||||
>
|
||||
<span className={cn("inline-flex rounded-full px-2 py-0.5 text-xs font-medium", item.soft)}>{item.label}</span>
|
||||
<span className="mt-2 block truncate text-base font-semibold tabular-nums text-[#231815] dark:text-white">
|
||||
{fmtNumber(totalFor(item.key))}
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-[11px] text-[#8B6B6B] dark:text-[#D8B8B8]">{item.description}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{metricCards({ includeAll: false })}
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 text-xs text-[#7A5A5A] dark:text-[#D8B8B8]">
|
||||
<span>每个点代表 1 天;当前显示 {metric === "all" ? "全部指标" : activeSeries.label},覆盖 {activeDays} 个活跃日。</span>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-8">
|
||||
<Maximize2 className="mr-1 size-4" />
|
||||
放大
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[min(1180px,calc(100vw-2rem))]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>个人使用趋势</DialogTitle>
|
||||
<DialogDescription>
|
||||
每个折线点代表一天。运行=发起次数,问答=有效提问,活跃=有行为的用户数,Token=模型输入输出消耗。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{chart(true)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{analysisDialog(
|
||||
"个人使用趋势",
|
||||
"每个折线点代表一天。运行=发起次数,问答=有效提问,活跃=有行为的用户数,Token=模型输入输出消耗。",
|
||||
)}
|
||||
</div>
|
||||
{chart(false)}
|
||||
</div>
|
||||
@@ -651,58 +695,11 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP
|
||||
每个折线点代表一天;点击指标卡可单独查看运行、问答、活跃或 Token,全部趋势会同时展示四条多色折线。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMetric("all")}
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-2 text-left transition hover:-translate-y-0.5 hover:shadow-sm",
|
||||
metric === "all"
|
||||
? "border-[#C1121F] bg-[#FFF1F1] shadow-sm dark:border-[#F2B8BB]/40 dark:bg-[#3A1215]"
|
||||
: "border-[#F1D6D6] bg-white dark:border-[#462123] dark:bg-[#230D0F]",
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex rounded-full bg-[#FFF1F1] px-2 py-0.5 text-xs font-medium text-[#C1121F]">全部趋势</span>
|
||||
<span className="mt-2 block text-lg font-semibold tabular-nums text-[#231815] dark:text-white">
|
||||
{fmtNumber(visible.reduce((sum, point) => sum + point.runs + point.questions + point.users, 0))}
|
||||
</span>
|
||||
</button>
|
||||
{series.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setMetric(item.key)}
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-2 text-left transition hover:-translate-y-0.5 hover:shadow-sm",
|
||||
metric === item.key
|
||||
? "border-[#C1121F] bg-[#FFF1F1] shadow-sm dark:border-[#F2B8BB]/40 dark:bg-[#3A1215]"
|
||||
: "border-[#F1D6D6] bg-white dark:border-[#462123] dark:bg-[#230D0F]",
|
||||
)}
|
||||
>
|
||||
<span className={cn("inline-flex rounded-full px-2 py-0.5 text-xs font-medium", item.soft)}>{item.label}</span>
|
||||
<span className="mt-2 block text-lg font-semibold tabular-nums text-[#111827] dark:text-white">
|
||||
{fmtNumber(visible.reduce((sum, point) => sum + Number(point[item.key] ?? 0), 0))}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Maximize2 className="mr-1 size-4" />
|
||||
放大分析
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[min(1180px,calc(100vw-2rem))]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>时间趋势分析</DialogTitle>
|
||||
<DialogDescription>
|
||||
当前查看 {metric === "all" ? "全部指标" : activeSeries.label},峰值 {peak.date} / {fmtNumber(Number(peak[activeMetric] ?? 0))}。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{chart(true)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="min-w-0 flex-1">{metricCards()}</div>
|
||||
{analysisDialog(
|
||||
"时间趋势分析",
|
||||
<>当前查看 {metric === "all" ? "全部指标" : activeSeries.label},峰值 {peak.date} / {fmtNumber(Number(peak[activeMetric] ?? 0))}。</>,
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-3 lg:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<div className="rounded-xl border border-[#F1D6D6] bg-white p-4 dark:border-[#462123] dark:bg-[#230D0F]">
|
||||
@@ -1260,14 +1257,14 @@ export default function AdminUserLeaderboardPage() {
|
||||
AI 报告
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[min(920px,calc(100vw-2rem))]">
|
||||
<DialogHeader>
|
||||
<DialogContent className="flex h-[92vh] w-[96vw] max-w-none flex-col gap-0 overflow-hidden p-0 sm:max-w-none">
|
||||
<DialogHeader className="shrink-0 border-b border-[#F1D6D6] px-6 py-5 pr-12 dark:border-[#462123]">
|
||||
<DialogTitle>AI 榜单分析报告</DialogTitle>
|
||||
<DialogDescription>
|
||||
基于当前时间段的问答、用户、技能、模型和趋势统计生成运营分析。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-[#F1D6D6] bg-white p-3 dark:border-[#462123] dark:bg-[#230D0F]">
|
||||
<div className="mx-6 mt-4 flex shrink-0 flex-wrap items-center justify-between gap-3 rounded-lg border border-[#F1D6D6] bg-white p-3 dark:border-[#462123] dark:bg-[#230D0F]">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<Select value={reportModelName} onValueChange={setReportModelName} disabled={reportGenerating || reportModelsLoading}>
|
||||
<SelectTrigger className="h-9 w-[260px] border-[#F1D6D6] bg-white dark:border-[#462123] dark:bg-[#19090A]">
|
||||
@@ -1292,7 +1289,7 @@ export default function AdminUserLeaderboardPage() {
|
||||
</Button>
|
||||
</div>
|
||||
{data && (
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="mx-6 mt-4 grid shrink-0 gap-3 md:grid-cols-3">
|
||||
<section className="rounded-lg border border-[#F1D6D6] bg-white p-3 dark:border-[#462123] dark:bg-[#230D0F]">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-[#231815] dark:text-white">
|
||||
<MessageSquareText className="size-4 text-[#C1121F]" />
|
||||
@@ -1339,7 +1336,7 @@ export default function AdminUserLeaderboardPage() {
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
<ScrollArea className="max-h-[70vh] rounded-lg border border-[#F1D6D6] bg-[#FFF7F7] p-4 dark:border-[#462123] dark:bg-[#19090A]">
|
||||
<ScrollArea className="mx-6 mt-4 min-h-0 flex-1 rounded-lg border border-[#F1D6D6] bg-[#FFF7F7] p-5 dark:border-[#462123] dark:bg-[#19090A]">
|
||||
{reportStatus === "idle" && !reportText && (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-[#7A5A5A] dark:text-[#D8B8B8]">
|
||||
选择模型后点击生成;同一时间段、同一模型和同一统计设置会直接读取已保存报告。
|
||||
@@ -1357,13 +1354,13 @@ export default function AdminUserLeaderboardPage() {
|
||||
</div>
|
||||
)}
|
||||
{reportText && (
|
||||
<div className="rounded-lg bg-white px-5 py-4 text-sm leading-7 text-[#231815] shadow-sm dark:bg-[#230D0F] dark:text-white [&_blockquote]:border-l-4 [&_blockquote]:border-[#C1121F] [&_blockquote]:bg-[#FFF7F7] [&_blockquote]:px-3 [&_blockquote]:py-2 [&_code]:rounded [&_code]:bg-[#FFF1F1] [&_code]:px-1 [&_h1]:mb-4 [&_h1]:text-xl [&_h1]:font-semibold [&_h2]:mb-3 [&_h2]:mt-5 [&_h2]:border-b [&_h2]:border-[#F1D6D6] [&_h2]:pb-2 [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:mb-2 [&_h3]:mt-4 [&_h3]:font-semibold [&_li]:my-1 [&_ol]:pl-5 [&_p]:my-2 [&_strong]:text-[#C1121F] [&_table]:my-3 [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#F1D6D6] [&_td]:p-2 [&_th]:border [&_th]:border-[#F1D6D6] [&_th]:bg-[#FFF1F1] [&_th]:p-2 [&_ul]:pl-5 dark:[&_blockquote]:bg-[#19090A] dark:[&_code]:bg-[#19090A] dark:[&_strong]:text-[#F2B8BB] dark:[&_th]:bg-[#19090A]">
|
||||
<div className="rounded-lg bg-white px-8 py-6 text-base leading-8 text-[#231815] shadow-sm dark:bg-[#230D0F] dark:text-white [&_blockquote]:border-l-4 [&_blockquote]:border-[#C1121F] [&_blockquote]:bg-[#FFF7F7] [&_blockquote]:px-4 [&_blockquote]:py-3 [&_code]:rounded [&_code]:bg-[#FFF1F1] [&_code]:px-1.5 [&_h1]:mb-5 [&_h1]:text-2xl [&_h1]:font-semibold [&_h2]:mb-4 [&_h2]:mt-7 [&_h2]:border-b [&_h2]:border-[#F1D6D6] [&_h2]:pb-3 [&_h2]:text-xl [&_h2]:font-semibold [&_h3]:mb-3 [&_h3]:mt-5 [&_h3]:text-lg [&_h3]:font-semibold [&_li]:my-1.5 [&_ol]:pl-6 [&_p]:my-3 [&_strong]:text-[#C1121F] [&_table]:my-4 [&_table]:w-full [&_table]:border-collapse [&_td]:border [&_td]:border-[#F1D6D6] [&_td]:p-3 [&_th]:border [&_th]:border-[#F1D6D6] [&_th]:bg-[#FFF1F1] [&_th]:p-3 [&_ul]:pl-6 dark:[&_blockquote]:bg-[#19090A] dark:[&_code]:bg-[#19090A] dark:[&_strong]:text-[#F2B8BB] dark:[&_th]:bg-[#19090A]">
|
||||
<MarkdownContent content={reportText} isLoading={reportGenerating} rehypePlugins={[]} />
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
{reportText && (
|
||||
<div className="flex justify-end">
|
||||
<div className="mt-4 flex shrink-0 justify-end border-t border-[#F1D6D6] px-6 py-4 dark:border-[#462123]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={exportReportAsWord}
|
||||
|
||||
Reference in new issue
Block a user