import { Activity, BadgeDollarSign, BrainCircuit, ChartNoAxesCombined, Gauge, Keyboard, Repeat2, Sparkles, Target, Users, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { adminApi } from "../../api/client"; import type { AnalyticsRate, ProductAnalyticsOverview, SortOrder, } from "../../api/types"; import { ChartToolbar } from "../../components/chart-toolbar"; import { CohortHeatmap } from "../../components/charts/cohort-heatmap"; import { ComparisonBarChart } from "../../components/charts/comparison-bar-chart"; import { FunnelChart } from "../../components/charts/funnel-chart"; import { FilterControl, ToggleFilter } from "../../components/filter-control"; import { Card, ErrorState, LoadingState, PageHeader, StatCard } from "../../components/primitives"; import { PeriodCaption } from "../../components/period-caption"; import { RangeControl } from "../../components/range-control"; import { SortControl } from "../../components/sort-control"; import { formatNumber } from "../../lib/format"; import { stableSort } from "../../lib/sort"; export function AnalyticsPage() { const [range, setRange] = useState("30d"); const [data, setData] = useState(); const [error, setError] = useState(); const [executionMode, setExecutionMode] = useState(""); const [aiSort, setAiSort] = useState<"successes" | "users">("successes"); const [aiOrder, setAiOrder] = useState("desc"); const [channelSort, setChannelSort] = useState<"installs" | "activated" | "rate">("installs"); const [channelOrder, setChannelOrder] = useState("desc"); const [hideUnknown, setHideUnknown] = useState(false); const [cohortOrder, setCohortOrder] = useState("desc"); const [minimumCohortSize, setMinimumCohortSize] = useState(0); const [matureOnly, setMatureOnly] = useState(false); const requestVersion = useRef(0); const load = useCallback(async () => { const version = ++requestVersion.current; setError(undefined); try { const response = await adminApi.productAnalytics(range); if (requestVersion.current === version) setData(response); } catch (requestError) { if (requestVersion.current === version) setError(requestError); } }, [range]); useEffect(() => { void load(); }, [load]); const visibleAiFeatures = useMemo( () => stableSort( (data?.aiFeatures ?? []).filter( (item) => !executionMode || item.executionMode === executionMode, ), (item) => (aiSort === "successes" ? item.successes : item.users), aiOrder, ), [aiOrder, aiSort, data?.aiFeatures, executionMode], ); const visibleChannels = useMemo( () => stableSort( (data?.growth.channels ?? []).filter( (channel) => !hideUnknown || channel.channel !== "UNKNOWN", ), (channel) => channelSort === "installs" ? channel.installations : channelSort === "activated" ? channel.activated : channel.activationRate.percent ?? -1, channelOrder, ), [channelOrder, channelSort, data?.growth.channels, hideUnknown], ); const visibleCohorts = useMemo( () => stableSort( (data?.retention ?? []).filter( (cohort) => cohort.size >= minimumCohortSize && (!matureOnly || cohort.d30?.percent != null), ), (cohort) => cohort.cohortDate, cohortOrder, ), [cohortOrder, data?.retention, matureOnly, minimumCohortSize], ); if (error) return void load()} />; if (!data) return ; const weeklyGrowth = data.northStar.weekOverWeekPercent; return (
} />
setCohortOrder(order)} /> setMinimumCohortSize(Number(value))} />
{ setAiSort(value); setAiOrder(order); }} /> {visibleAiFeatures.length === 0 ? (

当前筛选条件下暂无 AI 使用数据

) : ( ({ id: `${item.feature}-${item.executionMode}`, label: featureLabel(item.feature), value: item.successes, secondaryValue: item.users, hint: executionModeLabel(item.executionMode), }))} primaryLabel="成功次数" secondaryLabel="成功用户" primaryTone="violet" secondaryTone="success" /> )}
`${value.toFixed(1)}%`} /> `${value.toFixed(1)}%`} />
({ id: item.bucket, label: latencyBucketLabel(item.bucket), value: item.successful, secondaryValue: item.failed, }))} primaryLabel="成功" secondaryLabel="失败" primaryTone="success" secondaryTone="warning" emptyText="客户端 AI 终态事件暂无样本" />
{ setChannelSort(value); setChannelOrder(order); }} />
({ label: channelLabel(channel.channel), value: channel.installations, secondaryValue: channel.activated, hint: `激活率 ${rateLabel(channel.activationRate)}`, }))} primaryLabel="已完成 24h 观察安装" secondaryLabel="24 小时激活" emptyText="当前筛选条件下暂无渠道归因数据" />
); } function SectionHeader({ title, description, icon: Icon, }: { title: string; description: string; icon: typeof Activity; }) { return (

{title}

{description}

); } function MetricRows({ rows }: { rows: Array<[string, string]> }) { return (
{rows.map(([label, value]) => (
{label}
{value}
))}
); } function rateLabel(rate?: AnalyticsRate): string { return rate?.percent == null ? "—" : `${rate.percent.toFixed(1)}%`; } function optionalPercent(value?: number): string { return value == null ? "—" : `${value.toFixed(1)}%`; } function optionalDecimal(value?: number): string { return value == null ? "—" : value.toFixed(2); } function optionalNumber(value?: number): string { return value == null ? "—" : formatNumber(Math.round(value)); } function optionalMinutes(value?: number): string { return value == null ? "—" : `${Math.round(value)} 分钟`; } function signedPercent(value: number): string { return `${value >= 0 ? "+" : ""}${value.toFixed(1)}%`; } function latestMatureRetention( data: ProductAnalyticsOverview, key: "d1" | "d7" | "d30", ): string { return rateLabel(data.retention.find((cohort) => cohort[key]?.percent != null)?.[key]); } function channelLabel(channel: string): string { return { APP_STORE_ORGANIC: "App Store 自然量", REFERRAL: "用户邀请", SOCIAL_CONTENT: "社交 / 内容", UNKNOWN: "未知来源", }[channel] ?? channel; } function featureLabel(feature: string): string { return { TRANSCRIPTION: "语音转写", POLISH: "文字润色", AI_ASSISTANT: "AI 助手", AGENT: "Agent", HOTWORD: "快捷指令", OTHER: "其他", }[feature] ?? feature; } function executionModeLabel(mode: string): string { return { MANAGED: "托管", LOCAL: "本地", BYOK: "BYOK", }[mode] ?? mode; } function latencyBucketLabel(bucket: string): string { return { LT_1S: "< 1 秒", S1_TO_3: "1–3 秒", S3_TO_10: "3–10 秒", S10_TO_30: "10–30 秒", GTE_30S: "≥ 30 秒", }[bucket] ?? bucket; }