mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
Refactor overview widgets to use responsive grid sizing
This commit is contained in:
+48
-11
@@ -1,7 +1,7 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { loadPersistedApiKeys, replacePersistedApiKeys } from "./api-key-store";
|
||||
import { CONFIGDIR, CONFIG_FILE, GATEWAY_CONFIG_FILE } from "./constants";
|
||||
import { DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, TRAY_WINDOW_MODULE_IDS } from "../shared/app";
|
||||
import { DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, OVERVIEW_WIDGET_SIZE_VALUES, TRAY_SINGLETON_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS } from "../shared/app";
|
||||
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue, providerEndpointCanReceiveProviderApiKey } from "../shared/provider-presets";
|
||||
import type {
|
||||
AppConfig,
|
||||
@@ -528,7 +528,7 @@ function parseOverviewWidget(value: unknown): OverviewWidgetConfig | undefined {
|
||||
enabled: typeof value.enabled === "boolean" ? value.enabled : true,
|
||||
id: readString(value.id) || overviewWidgetId(type, metric),
|
||||
...(metric ? { metric } : {}),
|
||||
size: parseOverviewWidgetSize(value.size) ?? defaultOverviewWidgetSize(type),
|
||||
size: parseOverviewWidgetSize(value.size, type) ?? defaultOverviewWidgetSize(type),
|
||||
type,
|
||||
variant: parseOverviewWidgetVariant(value.variant) ?? defaultOverviewWidgetVariant(type)
|
||||
};
|
||||
@@ -538,8 +538,24 @@ function parseOverviewWidgetType(value: unknown): OverviewWidgetType | undefined
|
||||
return parseEnumValue(value, ["account-balance", "client-analysis", "metric", "provider-analysis", "system-status", "token-mix", "usage-trend"], undefined);
|
||||
}
|
||||
|
||||
function parseOverviewWidgetSize(value: unknown): OverviewWidgetSize | undefined {
|
||||
return parseEnumValue(value, ["small", "medium", "large", "wide", "full"], undefined);
|
||||
function parseOverviewWidgetSize(value: unknown, type: OverviewWidgetType): OverviewWidgetSize | undefined {
|
||||
const size = parseEnumValue(value, OVERVIEW_WIDGET_SIZE_VALUES, undefined);
|
||||
if (size) {
|
||||
return size;
|
||||
}
|
||||
if (value === "small") {
|
||||
return "1:1";
|
||||
}
|
||||
if (value === "medium" || value === "large") {
|
||||
return "2:2";
|
||||
}
|
||||
if (value === "wide") {
|
||||
return "3:2";
|
||||
}
|
||||
if (value === "full") {
|
||||
return type === "system-status" ? "4:1" : "4:2";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseOverviewWidgetVariant(value: unknown): OverviewWidgetVariant | undefined {
|
||||
@@ -552,18 +568,21 @@ function parseOverviewMetricKind(value: unknown): OverviewMetricKind | undefined
|
||||
|
||||
function defaultOverviewWidgetSize(type: OverviewWidgetType): OverviewWidgetSize {
|
||||
if (type === "metric") {
|
||||
return "small";
|
||||
return "1:1";
|
||||
}
|
||||
if (type === "token-mix") {
|
||||
return "medium";
|
||||
return "1:2";
|
||||
}
|
||||
if (type === "client-analysis" || type === "provider-analysis") {
|
||||
return "large";
|
||||
return "2:2";
|
||||
}
|
||||
if (type === "usage-trend") {
|
||||
return "wide";
|
||||
return "3:2";
|
||||
}
|
||||
return "full";
|
||||
if (type === "system-status") {
|
||||
return "4:1";
|
||||
}
|
||||
return "4:2";
|
||||
}
|
||||
|
||||
function defaultOverviewWidgetVariant(type: OverviewWidgetType): OverviewWidgetVariant {
|
||||
@@ -609,9 +628,9 @@ function parseTrayWidgets(value: unknown): TrayWidgetConfig[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return value
|
||||
return dedupeTraySingletonWidgets(value
|
||||
.map(parseTrayWidget)
|
||||
.filter((widget): widget is TrayWidgetConfig => Boolean(widget));
|
||||
.filter((widget): widget is TrayWidgetConfig => Boolean(widget)));
|
||||
}
|
||||
|
||||
function parseTrayWidget(value: unknown): TrayWidgetConfig | undefined {
|
||||
@@ -665,6 +684,24 @@ function trayWidgetId(type: TrayWidgetType): string {
|
||||
return type;
|
||||
}
|
||||
|
||||
function isTraySingletonWidgetType(type: TrayWidgetType): boolean {
|
||||
return (TRAY_SINGLETON_WIDGET_TYPES as readonly string[]).includes(type);
|
||||
}
|
||||
|
||||
function dedupeTraySingletonWidgets(widgets: TrayWidgetConfig[]): TrayWidgetConfig[] {
|
||||
const seenSingletons = new Set<TrayWidgetType>();
|
||||
return widgets.filter((widget) => {
|
||||
if (!isTraySingletonWidgetType(widget.type)) {
|
||||
return true;
|
||||
}
|
||||
if (seenSingletons.has(widget.type)) {
|
||||
return false;
|
||||
}
|
||||
seenSingletons.add(widget.type);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function trayWidgetsFromModules(modules: TrayWindowModuleId[], variants: TrayComponentVariants): TrayWidgetConfig[] {
|
||||
return modules
|
||||
.filter((moduleId): moduleId is TrayWidgetType => moduleId !== "footer")
|
||||
|
||||
@@ -203,7 +203,7 @@ export function OverviewView({
|
||||
>
|
||||
<SortableContext items={visibleWidgets.map((widget) => widget.id)} strategy={rectSortingStrategy}>
|
||||
<LayoutGroup>
|
||||
<section className="grid grid-cols-12 gap-4" data-overview-widget-grid>
|
||||
<section className="grid auto-rows-[132px] grid-cols-1 gap-4 sm:auto-rows-[140px] sm:grid-cols-2 xl:auto-rows-[148px] xl:grid-cols-4" data-overview-widget-grid>
|
||||
{visibleWidgets.map((widget) => (
|
||||
<SortableOverviewWidget editing={editing} key={widget.id} widget={widget} onSelect={() => setSelectedWidgetId(widget.id)}>
|
||||
<OverviewWidgetFrame
|
||||
@@ -222,7 +222,7 @@ export function OverviewView({
|
||||
</SortableOverviewWidget>
|
||||
))}
|
||||
{visibleWidgets.length === 0 ? (
|
||||
<div className="col-span-12 rounded-lg border border-dashed border-border bg-muted/30 px-4 py-10 text-center text-[12px] text-muted-foreground">
|
||||
<div className="col-span-1 rounded-lg border border-dashed border-border bg-muted/30 px-4 py-10 text-center text-[12px] text-muted-foreground sm:col-span-2 xl:col-span-4">
|
||||
{t("No widgets configured")}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -442,7 +442,7 @@ function SortableOverviewWidget({
|
||||
return (
|
||||
<motion.div
|
||||
className={cn(
|
||||
"min-w-0",
|
||||
"min-h-0 min-w-0",
|
||||
overviewWidgetSizeClass(widget.size),
|
||||
editing && "cursor-grab touch-none",
|
||||
isDragging && "relative z-20 cursor-grabbing opacity-70"
|
||||
@@ -476,7 +476,7 @@ function OverviewWidgetDragOverlay({
|
||||
widget: OverviewWidgetConfig;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("pointer-events-none opacity-95 shadow-2xl", overviewWidgetOverlaySizeClass(widget.size))}>
|
||||
<div className={cn("pointer-events-none overflow-hidden opacity-95 shadow-2xl", overviewWidgetOverlaySizeClass(widget.size))}>
|
||||
<OverviewWidgetRenderer
|
||||
providerAccounts={providerAccounts}
|
||||
setUsageRange={setUsageRange}
|
||||
@@ -510,7 +510,7 @@ function OverviewWidgetFrame({
|
||||
<div
|
||||
aria-selected={editing ? selected : undefined}
|
||||
className={cn(
|
||||
"group/overview-widget relative min-w-0 transition-opacity",
|
||||
"group/overview-widget relative h-full min-h-0 min-w-0 transition-opacity",
|
||||
editing && (selected
|
||||
? "rounded-xl outline outline-2 outline-primary outline-offset-2 ring-2 ring-primary/20"
|
||||
: "rounded-xl outline outline-2 outline-primary/35 outline-offset-2")
|
||||
@@ -537,25 +537,24 @@ function OverviewWidgetRenderer({
|
||||
usageStats: UsageStatsSnapshot;
|
||||
widget: OverviewWidgetConfig;
|
||||
}) {
|
||||
let content: ReactNode;
|
||||
if (widget.type === "system-status") {
|
||||
return <SystemStatusBar usageRange={usageRange} usageStats={usageStats} variant={widget.variant === "compact" ? "compact" : "timeline"} />;
|
||||
content = <SystemStatusBar usageRange={usageRange} usageStats={usageStats} variant={widget.variant === "compact" ? "compact" : "timeline"} />;
|
||||
} else if (widget.type === "account-balance") {
|
||||
content = <ProviderAccountsOverview accounts={providerAccounts} variant={overviewAccountVariant(widget.variant)} />;
|
||||
} else if (widget.type === "metric") {
|
||||
content = <OverviewMetricWidget metric={widget.metric ?? "requests"} totals={usageStats.totals} variant={overviewMetricVariant(widget.variant)} />;
|
||||
} else if (widget.type === "usage-trend") {
|
||||
content = <UsageTrendWidget setUsageRange={setUsageRange} usageRange={usageRange} usageStats={usageStats} variant={overviewTrendVariant(widget.variant)} />;
|
||||
} else if (widget.type === "token-mix") {
|
||||
content = <TokenMixOverviewWidget totals={usageStats.totals} variant={overviewTokenMixVariant(widget.variant)} />;
|
||||
} else if (widget.type === "client-analysis") {
|
||||
content = <OverviewAnalysisWidget kind="client" rows={usageStats.clientModels} variant={widget.variant === "compact" ? "compact" : "table"} />;
|
||||
} else {
|
||||
content = <OverviewAnalysisWidget kind="provider" rows={usageStats.providerModels} variant={widget.variant === "compact" ? "compact" : "table"} />;
|
||||
}
|
||||
if (widget.type === "account-balance") {
|
||||
return <ProviderAccountsOverview accounts={providerAccounts} variant={overviewAccountVariant(widget.variant)} />;
|
||||
}
|
||||
if (widget.type === "metric") {
|
||||
return <OverviewMetricWidget metric={widget.metric ?? "requests"} totals={usageStats.totals} variant={overviewMetricVariant(widget.variant)} />;
|
||||
}
|
||||
if (widget.type === "usage-trend") {
|
||||
return <UsageTrendWidget setUsageRange={setUsageRange} usageRange={usageRange} usageStats={usageStats} variant={overviewTrendVariant(widget.variant)} />;
|
||||
}
|
||||
if (widget.type === "token-mix") {
|
||||
return <TokenMixOverviewWidget totals={usageStats.totals} variant={overviewTokenMixVariant(widget.variant)} />;
|
||||
}
|
||||
if (widget.type === "client-analysis") {
|
||||
return <OverviewAnalysisWidget kind="client" rows={usageStats.clientModels} variant={widget.variant === "compact" ? "compact" : "table"} />;
|
||||
}
|
||||
return <OverviewAnalysisWidget kind="provider" rows={usageStats.providerModels} variant={widget.variant === "compact" ? "compact" : "table"} />;
|
||||
|
||||
return <div className="h-full min-h-0 min-w-0 overflow-hidden">{content}</div>;
|
||||
}
|
||||
|
||||
function OverviewMetricWidget({
|
||||
@@ -572,8 +571,8 @@ function OverviewMetricWidget({
|
||||
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardContent className="flex items-center justify-between gap-3 p-3">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<CardContent className="flex min-h-0 flex-1 items-center justify-between gap-3 p-3">
|
||||
<div className="min-w-0 truncate text-[12px] font-medium text-muted-foreground">{item.label}</div>
|
||||
<div className="shrink-0 text-[18px] font-semibold tracking-tight">{item.value}</div>
|
||||
</CardContent>
|
||||
@@ -583,8 +582,8 @@ function OverviewMetricWidget({
|
||||
|
||||
if (variant === "bar") {
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardContent className="p-3">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<CardContent className="min-h-0 flex-1 p-3">
|
||||
<div className="flex items-end justify-between gap-3">
|
||||
<div className="min-w-0 truncate text-[12px] font-medium text-muted-foreground">{item.label}</div>
|
||||
<div className="shrink-0 text-[18px] font-semibold tracking-tight">{item.value}</div>
|
||||
@@ -599,8 +598,8 @@ function OverviewMetricWidget({
|
||||
|
||||
if (variant === "ring") {
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardContent className="grid grid-cols-[58px_minmax(0,1fr)] items-center gap-3 p-3">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<CardContent className="grid min-h-0 flex-1 grid-cols-[58px_minmax(0,1fr)] items-center gap-3 p-3">
|
||||
<OverviewRingMetric ratio={item.ratio} tone={item.tone} />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[12px] font-medium text-muted-foreground">{item.label}</div>
|
||||
@@ -652,8 +651,8 @@ function UsageTrendWidget({
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<CardHeader className="shrink-0 flex-row items-center justify-between">
|
||||
<CardTitle>{t("Usage Trend")}</CardTitle>
|
||||
<div className="flex rounded-md border border-border bg-background p-0.5">
|
||||
{usageRangeOptions.map((option) => (
|
||||
@@ -672,8 +671,8 @@ function UsageTrendWidget({
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartFrame>
|
||||
<CardContent className="min-h-0 flex-1">
|
||||
<ChartFrame fill>
|
||||
{({ height, width }) => (
|
||||
<ComposedChart data={usageStats.series} height={height} margin={{ bottom: 4, left: 0, right: 8, top: 28 }} width={width}>
|
||||
<CartesianGrid stroke="#dfe3e8" strokeDasharray="3 3" vertical={false} />
|
||||
@@ -732,12 +731,12 @@ function TokenMixOverviewWidget({
|
||||
const total = tokenMix.reduce((sum, item) => sum + item.value, 0);
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<CardHeader className="shrink-0 flex-row items-center justify-between">
|
||||
<CardTitle>{t("Token Mix")}</CardTitle>
|
||||
<Badge variant="outline">{formatCompactNumber(totals.totalTokens)}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="min-h-0 flex-1 overflow-auto">
|
||||
{variant === "stacked" ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex h-3 overflow-hidden rounded-full bg-muted">
|
||||
@@ -749,7 +748,7 @@ function TokenMixOverviewWidget({
|
||||
</div>
|
||||
) : null}
|
||||
{variant === "donut" || variant === "pie" ? (
|
||||
<ChartFrame>
|
||||
<ChartFrame fill>
|
||||
{({ height, width }) => (
|
||||
<PieChart height={height} width={width}>
|
||||
<Tooltip content={<TokenTooltip />} />
|
||||
@@ -772,7 +771,7 @@ function TokenMixOverviewWidget({
|
||||
</ChartFrame>
|
||||
) : null}
|
||||
{variant === "bars" ? (
|
||||
<ChartFrame>
|
||||
<ChartFrame fill>
|
||||
{({ height, width }) => (
|
||||
<BarChart data={tokenMix} height={height} layout="vertical" margin={{ bottom: 8, left: 8, right: 12, top: 8 }} width={width}>
|
||||
<CartesianGrid stroke="#dfe3e8" strokeDasharray="3 3" horizontal={false} />
|
||||
@@ -832,12 +831,12 @@ function OverviewAnalysisWidget({
|
||||
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<CardHeader className="shrink-0 flex-row items-center justify-between">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<Badge variant="outline">{rows.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="min-h-0 flex-1 overflow-auto">
|
||||
{rows.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border bg-muted/30 px-3 py-7 text-center text-[12px] text-muted-foreground">{emptyLabel}</div>
|
||||
) : (
|
||||
@@ -860,12 +859,12 @@ function OverviewAnalysisWidget({
|
||||
|
||||
function overviewWidgetTemplates(): OverviewWidgetConfig[] {
|
||||
return [
|
||||
{ enabled: true, id: "system-status", size: "full", type: "system-status", variant: "timeline" },
|
||||
{ enabled: true, id: "account-balance", size: "full", type: "account-balance", variant: "cards" },
|
||||
{ enabled: true, id: "metric-requests", metric: "requests", size: "small", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "usage-trend", size: "wide", type: "usage-trend", variant: "composed" },
|
||||
{ enabled: true, id: "token-mix", size: "medium", type: "token-mix", variant: "bars" },
|
||||
{ enabled: true, id: "client-analysis", size: "large", type: "client-analysis", variant: "table" }
|
||||
{ enabled: true, id: "system-status", size: "4:1", type: "system-status", variant: "timeline" },
|
||||
{ enabled: true, id: "account-balance", size: "4:2", type: "account-balance", variant: "cards" },
|
||||
{ enabled: true, id: "metric-requests", metric: "requests", size: "1:1", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "usage-trend", size: "3:2", type: "usage-trend", variant: "composed" },
|
||||
{ enabled: true, id: "token-mix", size: "1:2", type: "token-mix", variant: "bars" },
|
||||
{ enabled: true, id: "client-analysis", size: "2:2", type: "client-analysis", variant: "table" }
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1018,19 +1017,55 @@ function overviewWidgetVariantOptions(type: OverviewWidgetType): Array<{ label:
|
||||
}
|
||||
|
||||
function overviewWidgetSizeClass(size: OverviewWidgetSize): string {
|
||||
if (size === "small") return "col-span-12 sm:col-span-6 xl:col-span-2";
|
||||
if (size === "medium") return "col-span-12 md:col-span-6 xl:col-span-4";
|
||||
if (size === "large") return "col-span-12 lg:col-span-6";
|
||||
if (size === "wide") return "col-span-12 lg:col-span-8";
|
||||
return "col-span-12";
|
||||
const { height, width } = overviewWidgetDimensions(size);
|
||||
return cn(overviewWidgetWidthClass(width), overviewWidgetHeightClass(height));
|
||||
}
|
||||
|
||||
function overviewWidgetOverlaySizeClass(size: OverviewWidgetSize): string {
|
||||
if (size === "small") return "w-[min(320px,calc(100vw-2rem))]";
|
||||
if (size === "medium") return "w-[min(460px,calc(100vw-2rem))]";
|
||||
if (size === "large") return "w-[min(640px,calc(100vw-2rem))]";
|
||||
if (size === "wide") return "w-[min(860px,calc(100vw-2rem))]";
|
||||
return "w-[min(960px,calc(100vw-2rem))]";
|
||||
const { height, width } = overviewWidgetDimensions(size);
|
||||
return cn(overviewWidgetOverlayWidthClass(width), overviewWidgetOverlayHeightClass(height));
|
||||
}
|
||||
|
||||
function overviewWidgetDimensions(size: OverviewWidgetSize): { height: 1 | 2 | 3 | 4; width: 1 | 2 | 3 | 4 } {
|
||||
const [widthText, heightText] = size.split(":");
|
||||
const width = overviewWidgetDimensionValue(widthText);
|
||||
const height = overviewWidgetDimensionValue(heightText);
|
||||
return { height, width };
|
||||
}
|
||||
|
||||
function overviewWidgetDimensionValue(value: string | undefined): 1 | 2 | 3 | 4 {
|
||||
if (value === "2") return 2;
|
||||
if (value === "3") return 3;
|
||||
if (value === "4") return 4;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function overviewWidgetWidthClass(width: 1 | 2 | 3 | 4): string {
|
||||
if (width === 1) return "col-span-1";
|
||||
if (width === 2) return "col-span-1 sm:col-span-2";
|
||||
if (width === 3) return "col-span-1 sm:col-span-2 xl:col-span-3";
|
||||
return "col-span-1 sm:col-span-2 xl:col-span-4";
|
||||
}
|
||||
|
||||
function overviewWidgetHeightClass(height: 1 | 2 | 3 | 4): string {
|
||||
if (height === 1) return "row-span-1";
|
||||
if (height === 2) return "row-span-2";
|
||||
if (height === 3) return "row-span-3";
|
||||
return "row-span-4";
|
||||
}
|
||||
|
||||
function overviewWidgetOverlayWidthClass(width: 1 | 2 | 3 | 4): string {
|
||||
if (width === 1) return "w-[min(260px,calc(100vw-2rem))]";
|
||||
if (width === 2) return "w-[min(536px,calc(100vw-2rem))]";
|
||||
if (width === 3) return "w-[min(812px,calc(100vw-2rem))]";
|
||||
return "w-[min(1088px,calc(100vw-2rem))]";
|
||||
}
|
||||
|
||||
function overviewWidgetOverlayHeightClass(height: 1 | 2 | 3 | 4): string {
|
||||
if (height === 1) return "h-[148px]";
|
||||
if (height === 2) return "h-[312px]";
|
||||
if (height === 3) return "h-[476px]";
|
||||
return "h-[640px]";
|
||||
}
|
||||
|
||||
function sameOverviewWidgetOrder(a: OverviewWidgetConfig[], b: OverviewWidgetConfig[]): boolean {
|
||||
@@ -1130,8 +1165,8 @@ function SystemStatusBar({
|
||||
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<Card className="min-w-0 border-border/70 bg-card">
|
||||
<CardContent className="flex min-w-0 items-center justify-between gap-3 p-4">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col border-border/70 bg-card">
|
||||
<CardContent className="flex min-h-0 min-w-0 flex-1 items-center justify-between gap-3 p-4">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className={cn("flex h-5 w-5 shrink-0 items-center justify-center rounded-full", systemStatusIconClass(overallTone))}>
|
||||
<StatusIcon className="h-3.5 w-3.5" />
|
||||
@@ -1150,8 +1185,8 @@ function SystemStatusBar({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="min-w-0 overflow-visible border-border/70 bg-card">
|
||||
<CardContent className="space-y-4 p-4">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col border-border/70 bg-card">
|
||||
<CardContent className="min-h-0 flex-1 space-y-4 overflow-hidden p-4">
|
||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||
<h2 className="truncate text-[15px] font-semibold tracking-tight">{t("System status")}</h2>
|
||||
<div className="flex shrink-0 items-center gap-2 text-[12px] font-medium text-muted-foreground">
|
||||
@@ -1231,12 +1266,12 @@ function ProviderAccountsOverview({
|
||||
.slice(0, 6);
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<CardHeader className="shrink-0 flex-row items-center justify-between">
|
||||
<CardTitle>{t("Account Balance")}</CardTitle>
|
||||
<Badge variant="outline">{accounts.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="min-h-0 flex-1 overflow-auto">
|
||||
{visibleAccounts.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border bg-muted/30 px-3 py-7 text-center text-[12px] text-muted-foreground">
|
||||
{t("No account balance connectors configured")}
|
||||
@@ -1897,16 +1932,16 @@ function UsageAnalysisCard({
|
||||
const t = useAppText();
|
||||
|
||||
return (
|
||||
<Card className="min-w-0">
|
||||
<CardHeader className="flex-row items-center justify-between">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col">
|
||||
<CardHeader className="shrink-0 flex-row items-center justify-between">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<Badge variant="outline">{rows.length}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="min-h-0 flex-1 overflow-auto">
|
||||
{rows.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border bg-muted/30 px-3 py-8 text-center text-[12px] text-muted-foreground">{emptyLabel}</div>
|
||||
) : (
|
||||
<div className="max-h-[420px] overflow-auto rounded-lg border border-border/60">
|
||||
<div className="h-full overflow-auto rounded-lg border border-border/60">
|
||||
<table className="min-w-[840px] w-full border-collapse text-left text-[11px]">
|
||||
<thead className="sticky top-0 z-10 bg-muted text-muted-foreground">
|
||||
<tr>
|
||||
@@ -2037,7 +2072,7 @@ function UsageTooltip({
|
||||
);
|
||||
}
|
||||
|
||||
function ChartFrame({ children }: { children: (size: { height: number; width: number }) => ReactNode }) {
|
||||
function ChartFrame({ children, fill = false }: { children: (size: { height: number; width: number }) => ReactNode; fill?: boolean }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [size, setSize] = useState({ height: 0, width: 0 });
|
||||
|
||||
@@ -2070,7 +2105,7 @@ function ChartFrame({ children }: { children: (size: { height: number; width: nu
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-[260px] min-w-0" ref={containerRef}>
|
||||
<div className={cn(fill ? "h-full min-h-[120px]" : "h-[260px]", "min-w-0")} ref={containerRef}>
|
||||
{size.height > 0 && size.width > 0 ? children(size) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Input, languageDisplayName, Layers3, Palette,
|
||||
PanelLeftOpen, Power, ReactNode, ResolvedLanguage, ResolvedTheme, Select, SelectControl,
|
||||
SettingsPageId, themeDisplayName, TrayComponentVariants, TrayWidgetConfig, TrayWidgetType, TrayWidgetVariant,
|
||||
trayMascotIconUrls, arrayMove, defaultTrayWidgetVariant, normalizeTrayWidget, normalizeTrayWidgets, trayWidgetVariantOptions, useMemo, useState,
|
||||
trayMascotIconUrls, arrayMove, defaultTrayWidgetVariant, isTraySingletonWidgetType, normalizeTrayWidget, normalizeTrayWidgets, Switch, trayWidgetVariantOptions, useMemo, useState,
|
||||
X
|
||||
} from "../shared";
|
||||
export function AppSettingsDialog({
|
||||
@@ -252,6 +252,13 @@ function TraySettingsPage({
|
||||
}
|
||||
|
||||
function addTrayWidget(template: TrayWidgetConfig) {
|
||||
const existingSingleton = isTraySingletonWidgetType(template.type)
|
||||
? widgets.find((widget) => widget.type === template.type)
|
||||
: undefined;
|
||||
if (existingSingleton) {
|
||||
setSelectedTrayWidgetId(existingSingleton.id);
|
||||
return;
|
||||
}
|
||||
const id = uniqueTrayWidgetId(widgets, template.id);
|
||||
const widget = normalizeTrayWidget({ ...template, id });
|
||||
if (!widget) {
|
||||
@@ -261,23 +268,52 @@ function TraySettingsPage({
|
||||
setSelectedTrayWidgetId(id);
|
||||
}
|
||||
|
||||
function toggleSingletonTrayWidget(template: TrayWidgetConfig, enabled: boolean) {
|
||||
const existingWidget = widgets.find((widget) => widget.type === template.type);
|
||||
if (enabled) {
|
||||
if (existingWidget) {
|
||||
setSelectedTrayWidgetId(existingWidget.id);
|
||||
return;
|
||||
}
|
||||
addTrayWidget(template);
|
||||
return;
|
||||
}
|
||||
if (!existingWidget) {
|
||||
return;
|
||||
}
|
||||
removeTrayWidget(existingWidget.id);
|
||||
}
|
||||
|
||||
function updateTrayWidget(id: string, patch: Partial<TrayWidgetConfig>) {
|
||||
commitWidgets(widgets.map((widget) => widget.id === id ? normalizeTrayWidget({ ...widget, ...patch }) ?? widget : widget));
|
||||
}
|
||||
|
||||
function changeSelectedTrayWidgetType(type: TrayWidgetType) {
|
||||
if (!selectedWidget) {
|
||||
return;
|
||||
}
|
||||
const existingSingleton = isTraySingletonWidgetType(type)
|
||||
? widgets.find((widget) => widget.type === type && widget.id !== selectedWidget.id)
|
||||
: undefined;
|
||||
if (existingSingleton) {
|
||||
const nextWidgets = widgets.filter((widget) => widget.id !== selectedWidget.id);
|
||||
commitWidgets(nextWidgets);
|
||||
setSelectedTrayWidgetId(existingSingleton.id);
|
||||
return;
|
||||
}
|
||||
updateTrayWidget(selectedWidget.id, { type, variant: defaultTrayWidgetVariant(type) });
|
||||
}
|
||||
|
||||
function changeTrayWidgetCategory(category: TrayComponentCategory) {
|
||||
if (!selectedWidget) {
|
||||
return;
|
||||
}
|
||||
const type = trayWidgetTypeForCategory(category, selectedWidget.type);
|
||||
updateTrayWidget(selectedWidget.id, { type, variant: defaultTrayWidgetVariant(type) });
|
||||
changeSelectedTrayWidgetType(type);
|
||||
}
|
||||
|
||||
function changeTrayWidgetData(type: TrayWidgetType) {
|
||||
if (!selectedWidget) {
|
||||
return;
|
||||
}
|
||||
updateTrayWidget(selectedWidget.id, { type, variant: defaultTrayWidgetVariant(type) });
|
||||
changeSelectedTrayWidgetType(type);
|
||||
}
|
||||
|
||||
function changeTrayWidgetVariant(variant: TrayWidgetVariant) {
|
||||
@@ -302,9 +338,19 @@ function TraySettingsPage({
|
||||
if (!selectedWidget || selectedWidgetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const nextWidgets = widgets.filter((widget) => widget.id !== selectedWidget.id);
|
||||
removeTrayWidget(selectedWidget.id);
|
||||
}
|
||||
|
||||
function removeTrayWidget(id: string) {
|
||||
const widgetIndex = widgets.findIndex((widget) => widget.id === id);
|
||||
if (widgetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const nextWidgets = widgets.filter((widget) => widget.id !== id);
|
||||
commitWidgets(nextWidgets);
|
||||
setSelectedTrayWidgetId(nextWidgets[Math.min(selectedWidgetIndex, nextWidgets.length - 1)]?.id);
|
||||
setSelectedTrayWidgetId((currentId) => currentId === id
|
||||
? nextWidgets[Math.min(widgetIndex, nextWidgets.length - 1)]?.id
|
||||
: currentId);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -335,8 +381,9 @@ function TraySettingsPage({
|
||||
<div className="grid grid-cols-1 gap-1.5">
|
||||
{paletteItems.map((option) => {
|
||||
const Icon = option.icon;
|
||||
const enabledSingleton = !option.repeatable && widgets.some((widget) => widget.type === option.template.type);
|
||||
|
||||
return (
|
||||
return option.repeatable ? (
|
||||
<Button
|
||||
className={cn(
|
||||
"flex min-h-[46px] w-full min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-[12px] font-medium transition-colors",
|
||||
@@ -358,6 +405,40 @@ function TraySettingsPage({
|
||||
+
|
||||
</span>
|
||||
</Button>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-[46px] w-full min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-[12px] font-medium transition-colors",
|
||||
enabledSingleton ? "bg-primary/5 text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
)}
|
||||
key={option.value}
|
||||
>
|
||||
<span className={cn(
|
||||
"flex h-6 w-6 shrink-0 items-center justify-center rounded-md",
|
||||
enabledSingleton ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<button
|
||||
className="min-w-0 flex-1 text-left"
|
||||
onClick={() => {
|
||||
const existingWidget = widgets.find((widget) => widget.type === option.template.type);
|
||||
if (existingWidget) {
|
||||
setSelectedTrayWidgetId(existingWidget.id);
|
||||
return;
|
||||
}
|
||||
toggleSingletonTrayWidget(option.template, true);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="block truncate">{option.label}</span>
|
||||
<span className="block truncate text-[10px] font-normal opacity-70">{option.description}</span>
|
||||
</button>
|
||||
<Switch
|
||||
checked={enabledSingleton}
|
||||
onCheckedChange={(checked) => toggleSingletonTrayWidget(option.template, checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -458,6 +539,7 @@ type TrayWidgetPaletteItem = {
|
||||
description: string;
|
||||
icon: typeof Layers3;
|
||||
label: string;
|
||||
repeatable: boolean;
|
||||
template: TrayWidgetConfig;
|
||||
value: TrayComponentCategory;
|
||||
};
|
||||
@@ -471,6 +553,7 @@ function trayWidgetPalette(copy: AppCopy): TrayWidgetPaletteItem[] {
|
||||
description: copy.settings.trayModuleSourceTabs,
|
||||
icon: Layers3,
|
||||
label: t("Provider component"),
|
||||
repeatable: false,
|
||||
template: { id: "source-tabs", type: "source-tabs" },
|
||||
value: "provider-tabs"
|
||||
},
|
||||
@@ -479,6 +562,7 @@ function trayWidgetPalette(copy: AppCopy): TrayWidgetPaletteItem[] {
|
||||
description: copy.settings.trayModuleHeader,
|
||||
icon: PanelLeftOpen,
|
||||
label: t("Header component"),
|
||||
repeatable: false,
|
||||
template: { id: "header", type: "header" },
|
||||
value: "header"
|
||||
},
|
||||
@@ -487,6 +571,7 @@ function trayWidgetPalette(copy: AppCopy): TrayWidgetPaletteItem[] {
|
||||
description: copy.settings.trayModuleAccount,
|
||||
icon: Database,
|
||||
label: t("Account component"),
|
||||
repeatable: true,
|
||||
template: { id: "account", type: "account", variant: defaultTrayWidgetVariant("account") },
|
||||
value: "account"
|
||||
},
|
||||
@@ -495,6 +580,7 @@ function trayWidgetPalette(copy: AppCopy): TrayWidgetPaletteItem[] {
|
||||
description: copy.settings.trayModuleTokenFlow,
|
||||
icon: Activity,
|
||||
label: t("Trend component"),
|
||||
repeatable: true,
|
||||
template: { id: "token-flow", type: "token-flow", variant: defaultTrayWidgetVariant("token-flow") },
|
||||
value: "trend"
|
||||
},
|
||||
@@ -503,6 +589,7 @@ function trayWidgetPalette(copy: AppCopy): TrayWidgetPaletteItem[] {
|
||||
description: copy.settings.trayModuleStats,
|
||||
icon: Gauge,
|
||||
label: t("Metric component"),
|
||||
repeatable: true,
|
||||
template: { id: "stats", type: "stats", variant: defaultTrayWidgetVariant("stats") },
|
||||
value: "metrics"
|
||||
},
|
||||
@@ -515,6 +602,7 @@ function trayWidgetPalette(copy: AppCopy): TrayWidgetPaletteItem[] {
|
||||
description: t("Token mix, rings, model share"),
|
||||
icon: Boxes,
|
||||
label: t("Breakdown component"),
|
||||
repeatable: true,
|
||||
template: { id: "token-mix", type: "token-mix", variant: defaultTrayWidgetVariant("token-mix") },
|
||||
value: "breakdown"
|
||||
}
|
||||
|
||||
@@ -122,6 +122,8 @@ import {
|
||||
DEFAULT_TRAY_COMPONENT_VARIANTS,
|
||||
DEFAULT_TRAY_WIDGETS,
|
||||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
OVERVIEW_WIDGET_SIZE_VALUES,
|
||||
TRAY_SINGLETON_WIDGET_TYPES,
|
||||
TRAY_WINDOW_MODULE_IDS
|
||||
} from "../../../shared/app";
|
||||
import type {
|
||||
@@ -230,7 +232,7 @@ export {
|
||||
cn, claudeCodeLogoUrl, codexLogoUrl, onboardingMascotSpriteUrl, anthropicProviderIconUrl, bailianProviderIconUrl, deepseekProviderIconUrl,
|
||||
geminiProviderIconUrl, mistralProviderIconUrl, moonshotProviderIconUrl, openaiProviderIconUrl, openrouterProviderIconUrl, siliconflowProviderIconUrl, zaiGlobalCodingProviderIconUrl,
|
||||
zaiGlobalGeneralProviderIconUrl, zhipuCnCodingProviderIconUrl, zhipuCnGeneralProviderIconUrl, trayCyanIconUrl, trayOrangeIconUrl, trayVioletIconUrl, BUILTIN_UNIMCP_PACKAGE,
|
||||
BUILTIN_UNIMCP_SERVER_NAME, BUILTIN_UNIMCP_VISION_TOOL_NAME, BUILTIN_UNIMCP_WEB_SEARCH_TOOL_NAME, DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, TRAY_WINDOW_MODULE_IDS,
|
||||
BUILTIN_UNIMCP_SERVER_NAME, BUILTIN_UNIMCP_VISION_TOOL_NAME, BUILTIN_UNIMCP_WEB_SEARCH_TOOL_NAME, DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, OVERVIEW_WIDGET_SIZE_VALUES, TRAY_SINGLETON_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS,
|
||||
customProviderPresetId, defaultProviderAccountConfig, findProviderPreset, findProviderPresetByBaseUrl, primaryProviderPresetEndpoint, providerApiKeySafetyIssue, providerEndpointCanReceiveProviderApiKey,
|
||||
providerIdentitySafetyIssue, providerPresets, standardProviderAccountConfig, normalizeProviderBaseUrl, providerUrlWithDefaultScheme
|
||||
};
|
||||
@@ -1823,11 +1825,7 @@ export const usageRangeOptions: Array<{ label: string; value: UsageStatsRange }>
|
||||
];
|
||||
|
||||
export const overviewWidgetSizeOptions: Array<{ label: string; value: OverviewWidgetSize }> = [
|
||||
{ label: "Small", value: "small" },
|
||||
{ label: "Medium", value: "medium" },
|
||||
{ label: "Large", value: "large" },
|
||||
{ label: "Wide", value: "wide" },
|
||||
{ label: "Full", value: "full" }
|
||||
...OVERVIEW_WIDGET_SIZE_VALUES.map((size) => ({ label: size, value: size }))
|
||||
];
|
||||
|
||||
export const overviewMetricOptions: Array<{ label: string; value: OverviewMetricKind }> = [
|
||||
@@ -2333,9 +2331,9 @@ export type MetricTone = "amber" | "blue" | "indigo" | "rose" | "slate" | "teal"
|
||||
|
||||
export function MetricCard({ label, tone, value }: { label: string; tone: MetricTone; value: string }) {
|
||||
return (
|
||||
<Card className="h-full min-w-0 overflow-hidden">
|
||||
<Card className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
|
||||
<div className={cn("h-1", metricToneBar(tone))} />
|
||||
<CardContent className="flex h-full min-h-[88px] flex-col justify-center">
|
||||
<CardContent className="flex min-h-[88px] flex-1 flex-col justify-center">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[11px] font-medium text-muted-foreground">{label}</div>
|
||||
<div className="mt-1 truncate text-[20px] font-semibold">{value}</div>
|
||||
@@ -3966,11 +3964,11 @@ export function normalizeTrayComponentVariants(value: unknown): TrayComponentVar
|
||||
|
||||
export function normalizeTrayWidgets(value: unknown, fallbackModules?: unknown, fallbackVariants?: unknown): TrayWidgetConfig[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return trayWidgetsFromModules(normalizeTrayWindowModules(fallbackModules), normalizeTrayComponentVariants(fallbackVariants));
|
||||
return dedupeTraySingletonWidgets(trayWidgetsFromModules(normalizeTrayWindowModules(fallbackModules), normalizeTrayComponentVariants(fallbackVariants)));
|
||||
}
|
||||
return value
|
||||
return dedupeTraySingletonWidgets(value
|
||||
.map(normalizeTrayWidget)
|
||||
.filter((widget): widget is TrayWidgetConfig => Boolean(widget));
|
||||
.filter((widget): widget is TrayWidgetConfig => Boolean(widget)));
|
||||
}
|
||||
|
||||
export function normalizeTrayWidget(value: unknown): TrayWidgetConfig | undefined {
|
||||
@@ -4067,6 +4065,24 @@ export function trayWidgetId(type: TrayWidgetType): string {
|
||||
return type;
|
||||
}
|
||||
|
||||
export function isTraySingletonWidgetType(type: TrayWidgetType): boolean {
|
||||
return (TRAY_SINGLETON_WIDGET_TYPES as readonly string[]).includes(type);
|
||||
}
|
||||
|
||||
function dedupeTraySingletonWidgets(widgets: TrayWidgetConfig[]): TrayWidgetConfig[] {
|
||||
const seenSingletons = new Set<TrayWidgetType>();
|
||||
return widgets.filter((widget) => {
|
||||
if (!isTraySingletonWidgetType(widget.type)) {
|
||||
return true;
|
||||
}
|
||||
if (seenSingletons.has(widget.type)) {
|
||||
return false;
|
||||
}
|
||||
seenSingletons.add(widget.type);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function trayWidgetsFromModules(modules: TrayWindowModuleId[], variants: TrayComponentVariants): TrayWidgetConfig[] {
|
||||
return modules
|
||||
.filter((moduleId): moduleId is TrayWidgetType => moduleId !== "footer")
|
||||
@@ -4110,7 +4126,7 @@ export function normalizeOverviewWidget(value: unknown): OverviewWidgetConfig |
|
||||
enabled: typeof value.enabled === "boolean" ? value.enabled : true,
|
||||
id: stringValue(value.id) || overviewWidgetId(type, metric),
|
||||
...(metric ? { metric } : {}),
|
||||
size: normalizeOverviewWidgetSize(value.size) ?? defaultOverviewWidgetSize(type),
|
||||
size: normalizeOverviewWidgetSize(value.size, type) ?? defaultOverviewWidgetSize(type),
|
||||
type,
|
||||
variant
|
||||
};
|
||||
@@ -4122,10 +4138,26 @@ export function normalizeOverviewWidgetType(value: unknown): OverviewWidgetType
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function normalizeOverviewWidgetSize(value: unknown): OverviewWidgetSize | undefined {
|
||||
return typeof value === "string" && ["small", "medium", "large", "wide", "full"].includes(value)
|
||||
? value as OverviewWidgetSize
|
||||
: undefined;
|
||||
export function normalizeOverviewWidgetSize(value: unknown, type: OverviewWidgetType): OverviewWidgetSize | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
if ((OVERVIEW_WIDGET_SIZE_VALUES as readonly string[]).includes(value)) {
|
||||
return value as OverviewWidgetSize;
|
||||
}
|
||||
if (value === "small") {
|
||||
return "1:1";
|
||||
}
|
||||
if (value === "medium" || value === "large") {
|
||||
return "2:2";
|
||||
}
|
||||
if (value === "wide") {
|
||||
return "3:2";
|
||||
}
|
||||
if (value === "full") {
|
||||
return type === "system-status" ? "4:1" : "4:2";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeOverviewMetricKind(value: unknown): OverviewMetricKind | undefined {
|
||||
@@ -4186,11 +4218,12 @@ export function normalizeOverviewWidgetVariant(type: OverviewWidgetType, value:
|
||||
}
|
||||
|
||||
export function defaultOverviewWidgetSize(type: OverviewWidgetType): OverviewWidgetSize {
|
||||
if (type === "metric") return "small";
|
||||
if (type === "token-mix") return "medium";
|
||||
if (type === "client-analysis" || type === "provider-analysis") return "large";
|
||||
if (type === "usage-trend") return "wide";
|
||||
return "full";
|
||||
if (type === "metric") return "1:1";
|
||||
if (type === "token-mix") return "1:2";
|
||||
if (type === "client-analysis" || type === "provider-analysis") return "2:2";
|
||||
if (type === "usage-trend") return "3:2";
|
||||
if (type === "system-status") return "4:1";
|
||||
return "4:2";
|
||||
}
|
||||
|
||||
export function defaultOverviewWidgetVariant(type: OverviewWidgetType): OverviewWidgetVariant {
|
||||
|
||||
@@ -87,8 +87,15 @@ export function TrayApp() {
|
||||
const weekTotals = snapshots["7d"].totals;
|
||||
const monthTotals = snapshots["30d"].totals;
|
||||
const topModel = snapshots["30d"].models[0];
|
||||
const hasProviderSwitcher = trayWidgets.some((widget) => widget.type === "source-tabs");
|
||||
const hasAnyVisibleModule = trayWidgets.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasProviderSwitcher && selectedProvider) {
|
||||
setSelectedProvider(undefined);
|
||||
}
|
||||
}, [hasProviderSwitcher, selectedProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProvider) {
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Power } from "lucide-react";
|
||||
import trayCyanIconUrl from "../../../../assets/tray-cyan.png";
|
||||
import trayOrangeIconUrl from "../../../../assets/tray-orange.png";
|
||||
import trayVioletIconUrl from "../../../../assets/tray-violet.png";
|
||||
import { DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, TRAY_WINDOW_MODULE_IDS } from "../../../shared/app";
|
||||
import { DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, TRAY_SINGLETON_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS } from "../../../shared/app";
|
||||
import type {
|
||||
AppConfig,
|
||||
ProviderAccountMeter,
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
|
||||
export {
|
||||
createContext, useCallback, useContext, useEffect, useMemo, useState, createRoot,
|
||||
Power, trayCyanIconUrl, trayOrangeIconUrl, trayVioletIconUrl, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, TRAY_WINDOW_MODULE_IDS
|
||||
Power, trayCyanIconUrl, trayOrangeIconUrl, trayVioletIconUrl, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, TRAY_SINGLETON_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS
|
||||
};
|
||||
export type {
|
||||
ReactNode, AppConfig, ProviderAccountMeter, ProviderAccountSnapshot, TrayComponentVariants, TrayWidgetConfig, TrayWidgetType, TrayWidgetVariant, TrayWindowModuleId, UsageComparisonRow,
|
||||
@@ -49,6 +49,7 @@ export const trayText: Record<ResolvedLanguage, Record<string, string>> = {
|
||||
"24h": "24 小时",
|
||||
"7d": "7 天",
|
||||
"30d": "30 天",
|
||||
"All": "全部",
|
||||
"Account": "账户",
|
||||
"All providers": "全部供应商",
|
||||
"Avg latency": "平均延迟",
|
||||
@@ -198,8 +199,8 @@ export function createSourceTabs(rows: UsageComparisonRow[], configuredProviders
|
||||
|
||||
return [
|
||||
{
|
||||
id: "overview",
|
||||
label: "Overview"
|
||||
id: "all",
|
||||
label: "All"
|
||||
},
|
||||
...providerTabs
|
||||
];
|
||||
@@ -237,13 +238,13 @@ export function normalizeTrayComponentVariants(value: unknown): TrayComponentVar
|
||||
export function normalizeTrayWidgets(value: unknown, fallbackModules?: unknown, fallbackVariants?: unknown): TrayWidgetConfig[] {
|
||||
if (!Array.isArray(value)) {
|
||||
if (Array.isArray(fallbackModules)) {
|
||||
return trayWidgetsFromModules(normalizeTrayWindowModules(fallbackModules as AppConfig["trayWindowModules"]), normalizeTrayComponentVariants(fallbackVariants));
|
||||
return dedupeTraySingletonWidgets(trayWidgetsFromModules(normalizeTrayWindowModules(fallbackModules as AppConfig["trayWindowModules"]), normalizeTrayComponentVariants(fallbackVariants)));
|
||||
}
|
||||
return DEFAULT_TRAY_WIDGETS.map((widget) => ({ ...widget }));
|
||||
}
|
||||
return value
|
||||
return dedupeTraySingletonWidgets(value
|
||||
.map(normalizeTrayWidget)
|
||||
.filter((widget): widget is TrayWidgetConfig => Boolean(widget));
|
||||
.filter((widget): widget is TrayWidgetConfig => Boolean(widget)));
|
||||
}
|
||||
|
||||
export function normalizeTrayWidget(value: unknown): TrayWidgetConfig | undefined {
|
||||
@@ -340,6 +341,24 @@ export function trayWidgetId(type: TrayWidgetType): string {
|
||||
return type;
|
||||
}
|
||||
|
||||
export function isTraySingletonWidgetType(type: TrayWidgetType): boolean {
|
||||
return (TRAY_SINGLETON_WIDGET_TYPES as readonly string[]).includes(type);
|
||||
}
|
||||
|
||||
function dedupeTraySingletonWidgets(widgets: TrayWidgetConfig[]): TrayWidgetConfig[] {
|
||||
const seenSingletons = new Set<TrayWidgetType>();
|
||||
return widgets.filter((widget) => {
|
||||
if (!isTraySingletonWidgetType(widget.type)) {
|
||||
return true;
|
||||
}
|
||||
if (seenSingletons.has(widget.type)) {
|
||||
return false;
|
||||
}
|
||||
seenSingletons.add(widget.type);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function trayWidgetsFromModules(modules: TrayWindowModuleId[], variants: TrayComponentVariants): TrayWidgetConfig[] {
|
||||
return modules
|
||||
.filter((moduleId): moduleId is TrayWidgetType => moduleId !== "footer")
|
||||
|
||||
+33
-13
@@ -528,7 +528,26 @@ export type OverviewWidgetType =
|
||||
| "token-mix"
|
||||
| "usage-trend";
|
||||
|
||||
export type OverviewWidgetSize = "small" | "medium" | "large" | "wide" | "full";
|
||||
export const OVERVIEW_WIDGET_SIZE_VALUES = [
|
||||
"1:1",
|
||||
"2:1",
|
||||
"3:1",
|
||||
"4:1",
|
||||
"1:2",
|
||||
"2:2",
|
||||
"3:2",
|
||||
"4:2",
|
||||
"1:3",
|
||||
"2:3",
|
||||
"3:3",
|
||||
"4:3",
|
||||
"1:4",
|
||||
"2:4",
|
||||
"3:4",
|
||||
"4:4"
|
||||
] as const;
|
||||
|
||||
export type OverviewWidgetSize = typeof OVERVIEW_WIDGET_SIZE_VALUES[number];
|
||||
export type OverviewWidgetVariant =
|
||||
| "area"
|
||||
| "bar"
|
||||
@@ -567,18 +586,18 @@ export type OverviewWidgetConfig = {
|
||||
};
|
||||
|
||||
export const DEFAULT_OVERVIEW_WIDGETS: OverviewWidgetConfig[] = [
|
||||
{ enabled: true, id: "system-status", size: "full", type: "system-status", variant: "timeline" },
|
||||
{ enabled: true, id: "account-balance", size: "full", type: "account-balance", variant: "cards" },
|
||||
{ enabled: true, id: "metric-requests", metric: "requests", size: "small", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-input-tokens", metric: "input-tokens", size: "small", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-output-tokens", metric: "output-tokens", size: "small", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-cache-tokens", metric: "cache-tokens", size: "small", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-cache-ratio", metric: "cache-ratio", size: "small", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-estimated-cost", metric: "estimated-cost", size: "small", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "usage-trend", size: "wide", type: "usage-trend", variant: "composed" },
|
||||
{ enabled: true, id: "token-mix", size: "medium", type: "token-mix", variant: "bars" },
|
||||
{ enabled: true, id: "client-analysis", size: "large", type: "client-analysis", variant: "table" },
|
||||
{ enabled: true, id: "provider-analysis", size: "large", type: "provider-analysis", variant: "table" }
|
||||
{ enabled: true, id: "system-status", size: "4:1", type: "system-status", variant: "timeline" },
|
||||
{ enabled: true, id: "account-balance", size: "4:2", type: "account-balance", variant: "cards" },
|
||||
{ enabled: true, id: "metric-requests", metric: "requests", size: "1:1", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-input-tokens", metric: "input-tokens", size: "1:1", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-output-tokens", metric: "output-tokens", size: "1:1", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-cache-tokens", metric: "cache-tokens", size: "1:1", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-cache-ratio", metric: "cache-ratio", size: "1:1", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "metric-estimated-cost", metric: "estimated-cost", size: "1:1", type: "metric", variant: "card" },
|
||||
{ enabled: true, id: "usage-trend", size: "3:2", type: "usage-trend", variant: "composed" },
|
||||
{ enabled: true, id: "token-mix", size: "1:2", type: "token-mix", variant: "bars" },
|
||||
{ enabled: true, id: "client-analysis", size: "2:2", type: "client-analysis", variant: "table" },
|
||||
{ enabled: true, id: "provider-analysis", size: "2:2", type: "provider-analysis", variant: "table" }
|
||||
];
|
||||
|
||||
export const TRAY_WINDOW_MODULE_IDS = [
|
||||
@@ -595,6 +614,7 @@ export const TRAY_WINDOW_MODULE_IDS = [
|
||||
|
||||
export type TrayWindowModuleId = (typeof TRAY_WINDOW_MODULE_IDS)[number];
|
||||
export type TrayWidgetType = Exclude<TrayWindowModuleId, "footer">;
|
||||
export const TRAY_SINGLETON_WIDGET_TYPES = ["source-tabs", "header"] as const satisfies readonly TrayWidgetType[];
|
||||
|
||||
export type TrayWidgetConfig = {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user