Add share widget defaults and PNG export support

This commit is contained in:
musistudio 2026-06-30 17:07:28 +08:00
parent 437cf29d3e
commit 83ea5d8b5b
13 changed files with 1385 additions and 42 deletions

View file

@ -702,7 +702,7 @@ function parseOverviewWidget(value: unknown): OverviewWidgetConfig | undefined {
}
function parseOverviewWidgetType(value: unknown): OverviewWidgetType | undefined {
return parseEnumValue(value, ["account-balance", "client-analysis", "metric", "model-distribution", "provider-analysis", "system-status", "token-activity", "token-mix", "usage-trend"], undefined);
return parseEnumValue(value, ["account-balance", "client-analysis", "metric", "model-distribution", "provider-analysis", "share-fuel-cockpit", "share-model-leaderboard", "share-route-map", "share-spend-receipt", "share-token-calendar", "share-usage-wrapped", "system-status", "token-activity", "token-mix", "usage-trend"], undefined);
}
function parseOverviewWidgetSize(value: unknown, type: OverviewWidgetType): OverviewWidgetSize | undefined {
@ -755,6 +755,9 @@ function defaultOverviewWidgetSize(type: OverviewWidgetType): OverviewWidgetSize
if (type === "system-status") {
return "4:1";
}
if (isShareOverviewWidgetType(type)) {
return "1:4";
}
return "4:2";
}
@ -780,6 +783,9 @@ function defaultOverviewWidgetVariant(type: OverviewWidgetType): OverviewWidgetV
if (type === "system-status") {
return "timeline";
}
if (isShareOverviewWidgetType(type)) {
return "card";
}
return "table";
}
@ -787,6 +793,15 @@ function overviewWidgetId(type: OverviewWidgetType, metric?: OverviewMetricKind)
return type === "metric" ? `metric-${metric ?? "requests"}` : type;
}
function isShareOverviewWidgetType(type: OverviewWidgetType): boolean {
return type === "share-fuel-cockpit" ||
type === "share-model-leaderboard" ||
type === "share-route-map" ||
type === "share-spend-receipt" ||
type === "share-token-calendar" ||
type === "share-usage-wrapped";
}
function parseTrayIconPreference(value: unknown): TrayIconPreference | undefined {
if (value === "random" || value === "violet" || value === "orange" || value === "cyan" || value === "progress") {
return value;

View file

@ -1,7 +1,9 @@
import { app, BrowserWindow, dialog, ipcMain, shell, type OpenDialogOptions, type SaveDialogOptions } from "electron";
import { app, BrowserWindow, dialog, ipcMain, shell, type OpenDialogOptions, type Rectangle, type SaveDialogOptions } from "electron";
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
import net from "node:net";
import path from "node:path";
import { deflateSync, inflateSync } from "node:zlib";
import { loadPersistedAppSetting, replacePersistedAppSetting } from "./app-config-store";
import { builtInBrowserService } from "./built-in-browser";
import { scanBotHandoffBluetoothTargets, scanBotHandoffWifiTargets } from "./bot-handoff-scan-service";
@ -29,7 +31,7 @@ import trayController from "./tray-controller";
import { appUpdateService } from "./update-service";
import { getUsageStats } from "./usage-store";
import windowsManager from "./windows";
import type { AgentAnalysisFilter, AgentAnalysisTracePayloadRequest, ApiKeyConfig, AppConfig, AppDataExportResult, AppInfo, AppSaveConfigOptions, BotGatewayQrLoginCancelRequest, BotGatewayQrLoginStartRequest, BotGatewayQrLoginWaitRequest, BotGatewayQrWindowCloseRequest, BotGatewayQrWindowOpenRequest, GatewayPluginAppConfig, GatewayProviderConnectivityCheckRequest, GatewayProviderProbeCandidatesRequest, GatewayProviderProbeRequest, GatewayStatus, LocalAgentProviderImportRequest, PluginDependency, PluginDirectorySelection, PluginMarketplaceEntry, ProfileApplyResult, ProfileOpenRequest, ProviderAccountSnapshotRequestOptions, ProviderAccountTestRequest, ProviderCatalogModelsRequest, ProviderIconDetectionRequest, ProviderManifestFetchRequest, RequestLogListFilter, UsageStatsFilter, UsageStatsRange } from "../shared/app";
import type { AgentAnalysisFilter, AgentAnalysisTracePayloadRequest, ApiKeyConfig, AppCaptureElementPngRequest, AppCaptureElementPngResult, AppConfig, AppDataExportResult, AppImageExportTargetRequest, AppImageExportTargetResult, AppInfo, AppSaveConfigOptions, BotGatewayQrLoginCancelRequest, BotGatewayQrLoginStartRequest, BotGatewayQrLoginWaitRequest, BotGatewayQrWindowCloseRequest, BotGatewayQrWindowOpenRequest, GatewayPluginAppConfig, GatewayProviderConnectivityCheckRequest, GatewayProviderProbeCandidatesRequest, GatewayProviderProbeRequest, GatewayStatus, LocalAgentProviderImportRequest, PluginDependency, PluginDirectorySelection, PluginMarketplaceEntry, ProfileApplyResult, ProfileOpenRequest, ProviderAccountSnapshotRequestOptions, ProviderAccountTestRequest, ProviderCatalogModelsRequest, ProviderIconDetectionRequest, ProviderManifestFetchRequest, RequestLogListFilter, UsageStatsFilter, UsageStatsRange } from "../shared/app";
const pluginMarketplace: PluginMarketplaceEntry[] = [
{
@ -50,6 +52,7 @@ const pluginMarketplace: PluginMarketplaceEntry[] = [
}
];
const onboardingFinishedAtSettingKey = "onboardingFinishedAt";
const imageExportTargets = new Map<string, string>();
ipcMain.handle(IPC_CHANNELS.appGetInfo, () => {
return {
@ -70,6 +73,12 @@ ipcMain.handle(IPC_CHANNELS.appGetInfo, () => {
ipcMain.handle(IPC_CHANNELS.appExportData, async (event): Promise<AppDataExportResult> => {
return exportAppData(BrowserWindow.fromWebContents(event.sender));
});
ipcMain.handle(IPC_CHANNELS.appCaptureElementPng, async (event, request: AppCaptureElementPngRequest): Promise<AppCaptureElementPngResult> => {
return captureElementPng(BrowserWindow.fromWebContents(event.sender), request);
});
ipcMain.handle(IPC_CHANNELS.appPrepareImageExportTarget, async (event, request: AppImageExportTargetRequest): Promise<AppImageExportTargetResult> => {
return prepareImageExportTarget(BrowserWindow.fromWebContents(event.sender), request);
});
ipcMain.handle(IPC_CHANNELS.appGetConfig, () => loadAppConfig());
ipcMain.handle(IPC_CHANNELS.appGetOnboardingFinished, async () => {
@ -539,6 +548,43 @@ async function exportAppData(window: BrowserWindow | null): Promise<AppDataExpor
};
}
async function captureElementPng(window: BrowserWindow | null, request: AppCaptureElementPngRequest): Promise<AppCaptureElementPngResult> {
if (!window) {
throw new Error("Window is unavailable.");
}
const rect = sanitizeCaptureRect(request.rect);
const targetFile = request.exportId ? consumeImageExportTarget(request.exportId) : undefined;
const result = targetFile
? { canceled: false, filePath: targetFile }
: await dialog.showSaveDialog(window, shareCardSaveDialogOptions(request.fileName));
if (result.canceled || !result.filePath) {
return { canceled: true };
}
const image = await window.webContents.capturePage(rect);
const png = image.toPNG();
writeFileSync(result.filePath, pngWithExportProcessing(png, request, rect.width), { mode: 0o600 });
return { canceled: false, file: result.filePath };
}
async function prepareImageExportTarget(window: BrowserWindow | null, request: AppImageExportTargetRequest): Promise<AppImageExportTargetResult> {
const result = window
? await dialog.showSaveDialog(window, shareCardSaveDialogOptions(request.fileName))
: await dialog.showSaveDialog(shareCardSaveDialogOptions(request.fileName));
if (result.canceled || !result.filePath) {
return { canceled: true };
}
const exportId = randomUUID();
imageExportTargets.set(exportId, result.filePath);
return {
canceled: false,
exportId,
file: result.filePath
};
}
function dataExportSaveDialogOptions(exportedAt: string): SaveDialogOptions {
return {
buttonLabel: "Export",
@ -550,6 +596,321 @@ function dataExportSaveDialogOptions(exportedAt: string): SaveDialogOptions {
};
}
function shareCardSaveDialogOptions(fileName: string): SaveDialogOptions {
return {
buttonLabel: "Save image",
defaultPath: path.join(app.getPath("downloads"), safePngFileName(fileName)),
filters: [
{ extensions: ["png"], name: "PNG image" }
],
title: "Save image"
};
}
function sanitizeCaptureRect(rect: AppCaptureElementPngRequest["rect"]): Rectangle {
const x = finiteNumber(rect?.x, "capture x");
const y = finiteNumber(rect?.y, "capture y");
const width = finiteNumber(rect?.width, "capture width");
const height = finiteNumber(rect?.height, "capture height");
if (width <= 0 || height <= 0) {
throw new Error("Capture area must not be empty.");
}
return {
height: Math.ceil(height),
width: Math.ceil(width),
x: Math.max(0, Math.floor(x)),
y: Math.max(0, Math.floor(y))
};
}
function finiteNumber(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`Invalid ${label}.`);
}
return value;
}
function safePngFileName(value: string): string {
const raw = typeof value === "string" ? value : "";
const safe = path.basename(raw).replace(/[<>:"/\\|?*\x00-\x1f]/g, "-").trim() || "ccr-share-card.png";
return safe.toLowerCase().endsWith(".png") ? safe : `${safe}.png`;
}
function consumeImageExportTarget(exportId: string): string {
const target = imageExportTargets.get(exportId);
imageExportTargets.delete(exportId);
if (!target) {
throw new Error("Image export target is unavailable.");
}
return target;
}
const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
type DecodedPngPixels = {
bitDepth: number;
colorType: 2 | 6;
height: number;
pixels: Uint8Array;
width: number;
};
function pngWithExportProcessing(png: Buffer, request: AppCaptureElementPngRequest, cssWidth: number): Buffer {
const radius = typeof request.borderRadius === "number" && Number.isFinite(request.borderRadius) ? Math.max(0, request.borderRadius) : 0;
const outputWidth = sanitizePngOutputDimension(request.output?.width);
const outputHeight = sanitizePngOutputDimension(request.output?.height);
if (radius <= 0 && (!outputWidth || !outputHeight)) {
return png;
}
try {
const decoded = decodePngPixels(png);
let width = decoded.width;
let height = decoded.height;
let rgba = pngPixelsToRgba(decoded);
if (outputWidth && outputHeight && (outputWidth !== width || outputHeight !== height)) {
rgba = resizeRgbaBilinear(rgba, width, height, outputWidth, outputHeight);
width = outputWidth;
height = outputHeight;
}
if (radius > 0 && cssWidth > 0) {
const pixelRadius = Math.min(width / 2, height / 2, radius * width / cssWidth);
applyRoundedAlphaMask(rgba, width, height, pixelRadius);
}
return encodeRgbaPng(width, height, rgba);
} catch (error) {
console.warn(`[export] Failed to process exported PNG: ${formatError(error)}`);
return png;
}
}
function sanitizePngOutputDimension(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) {
return undefined;
}
const rounded = Math.round(value);
return rounded > 0 && rounded <= 4096 ? rounded : undefined;
}
function decodePngPixels(png: Buffer): DecodedPngPixels {
if (png.length < 33 || !png.subarray(0, pngSignature.length).equals(pngSignature)) {
throw new Error("Invalid PNG file.");
}
let offset = pngSignature.length;
let width = 0;
let height = 0;
let bitDepth = 0;
let colorType = 0;
let interlaceMethod = 0;
const idatChunks: Buffer[] = [];
while (offset + 12 <= png.length) {
const length = png.readUInt32BE(offset);
offset += 4;
const type = png.toString("ascii", offset, offset + 4);
offset += 4;
const data = png.subarray(offset, offset + length);
offset += length + 4;
if (type === "IHDR") {
width = data.readUInt32BE(0);
height = data.readUInt32BE(4);
bitDepth = data[8];
colorType = data[9];
interlaceMethod = data[12];
} else if (type === "IDAT") {
idatChunks.push(Buffer.from(data));
} else if (type === "IEND") {
break;
}
}
if (width <= 0 || height <= 0 || bitDepth !== 8 || (colorType !== 2 && colorType !== 6) || interlaceMethod !== 0) {
throw new Error("Unsupported PNG format.");
}
const channels = colorType === 6 ? 4 : 3;
const stride = width * channels;
const inflated = inflateSync(Buffer.concat(idatChunks));
const pixels = new Uint8Array(width * height * channels);
let sourceOffset = 0;
let previousRow = new Uint8Array(stride);
for (let y = 0; y < height; y += 1) {
const filter = inflated[sourceOffset];
sourceOffset += 1;
const row = new Uint8Array(stride);
for (let x = 0; x < stride; x += 1) {
const raw = inflated[sourceOffset + x];
const left = x >= channels ? row[x - channels] : 0;
const up = previousRow[x] ?? 0;
const upLeft = x >= channels ? previousRow[x - channels] ?? 0 : 0;
row[x] = unfilterPngByte(filter, raw, left, up, upLeft);
}
pixels.set(row, y * stride);
previousRow = row;
sourceOffset += stride;
}
return {
bitDepth,
colorType: colorType as 2 | 6,
height,
pixels,
width
};
}
function unfilterPngByte(filter: number, raw: number, left: number, up: number, upLeft: number): number {
if (filter === 0) return raw;
if (filter === 1) return (raw + left) & 0xff;
if (filter === 2) return (raw + up) & 0xff;
if (filter === 3) return (raw + Math.floor((left + up) / 2)) & 0xff;
if (filter === 4) return (raw + pngPaethPredictor(left, up, upLeft)) & 0xff;
throw new Error(`Unsupported PNG filter: ${filter}`);
}
function pngPaethPredictor(left: number, up: number, upLeft: number): number {
const estimate = left + up - upLeft;
const leftDistance = Math.abs(estimate - left);
const upDistance = Math.abs(estimate - up);
const upLeftDistance = Math.abs(estimate - upLeft);
if (leftDistance <= upDistance && leftDistance <= upLeftDistance) return left;
if (upDistance <= upLeftDistance) return up;
return upLeft;
}
function pngPixelsToRgba(decoded: DecodedPngPixels): Uint8Array {
const rgba = new Uint8Array(decoded.width * decoded.height * 4);
const sourceChannels = decoded.colorType === 6 ? 4 : 3;
for (let source = 0, target = 0; source < decoded.pixels.length; source += sourceChannels, target += 4) {
rgba[target] = decoded.pixels[source];
rgba[target + 1] = decoded.pixels[source + 1];
rgba[target + 2] = decoded.pixels[source + 2];
rgba[target + 3] = sourceChannels === 4 ? decoded.pixels[source + 3] : 255;
}
return rgba;
}
function resizeRgbaBilinear(source: Uint8Array, sourceWidth: number, sourceHeight: number, targetWidth: number, targetHeight: number): Uint8Array {
const target = new Uint8Array(targetWidth * targetHeight * 4);
const xRatio = targetWidth > 1 ? (sourceWidth - 1) / (targetWidth - 1) : 0;
const yRatio = targetHeight > 1 ? (sourceHeight - 1) / (targetHeight - 1) : 0;
for (let y = 0; y < targetHeight; y += 1) {
const sourceY = y * yRatio;
const y0 = Math.floor(sourceY);
const y1 = Math.min(sourceHeight - 1, y0 + 1);
const yWeight = sourceY - y0;
for (let x = 0; x < targetWidth; x += 1) {
const sourceX = x * xRatio;
const x0 = Math.floor(sourceX);
const x1 = Math.min(sourceWidth - 1, x0 + 1);
const xWeight = sourceX - x0;
const targetOffset = (y * targetWidth + x) * 4;
const topLeft = (y0 * sourceWidth + x0) * 4;
const topRight = (y0 * sourceWidth + x1) * 4;
const bottomLeft = (y1 * sourceWidth + x0) * 4;
const bottomRight = (y1 * sourceWidth + x1) * 4;
for (let channel = 0; channel < 4; channel += 1) {
const top = source[topLeft + channel] * (1 - xWeight) + source[topRight + channel] * xWeight;
const bottom = source[bottomLeft + channel] * (1 - xWeight) + source[bottomRight + channel] * xWeight;
target[targetOffset + channel] = Math.round(top * (1 - yWeight) + bottom * yWeight);
}
}
}
return target;
}
function applyRoundedAlphaMask(rgba: Uint8Array, width: number, height: number, radius: number): void {
if (radius <= 0) {
return;
}
const halfWidth = width / 2;
const halfHeight = height / 2;
const innerHalfWidth = Math.max(0, halfWidth - radius);
const innerHalfHeight = Math.max(0, halfHeight - radius);
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
const qx = Math.abs(x + 0.5 - halfWidth) - innerHalfWidth;
const qy = Math.abs(y + 0.5 - halfHeight) - innerHalfHeight;
const outside = Math.hypot(Math.max(qx, 0), Math.max(qy, 0));
const inside = Math.min(Math.max(qx, qy), 0);
const distance = outside + inside - radius;
const coverage = Math.max(0, Math.min(1, 0.5 - distance));
if (coverage >= 1) {
continue;
}
const alphaOffset = (y * width + x) * 4 + 3;
rgba[alphaOffset] = Math.round(rgba[alphaOffset] * coverage);
}
}
}
function encodeRgbaPng(width: number, height: number, rgba: Uint8Array): Buffer {
const stride = width * 4;
const scanlines = Buffer.alloc((stride + 1) * height);
for (let y = 0; y < height; y += 1) {
const target = y * (stride + 1);
scanlines[target] = 0;
scanlines.set(rgba.subarray(y * stride, (y + 1) * stride), target + 1);
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8;
ihdr[9] = 6;
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
return Buffer.concat([
pngSignature,
pngChunk("IHDR", ihdr),
pngChunk("IDAT", deflateSync(scanlines)),
pngChunk("IEND", Buffer.alloc(0))
]);
}
function pngChunk(type: string, data: Buffer): Buffer {
const typeBuffer = Buffer.from(type, "ascii");
const chunk = Buffer.alloc(12 + data.length);
chunk.writeUInt32BE(data.length, 0);
typeBuffer.copy(chunk, 4);
data.copy(chunk, 8);
chunk.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 8 + data.length);
return chunk;
}
let crc32Table: Uint32Array | undefined;
function crc32(buffer: Buffer): number {
const table = crc32Table ?? createCrc32Table();
crc32Table = table;
let crc = 0xffffffff;
for (const byte of buffer) {
crc = table[(crc ^ byte) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
function createCrc32Table(): Uint32Array {
const table = new Uint32Array(256);
for (let index = 0; index < table.length; index += 1) {
let value = index;
for (let bit = 0; bit < 8; bit += 1) {
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
}
table[index] = value >>> 0;
}
return table;
}
function readDataExportFiles(): Array<{ base64: string; name: string; path: string; sizeBytes: number }> {
const files: Array<{ base64: string; name: string; path: string; sizeBytes: number }> = [];
for (const file of dataExportCandidateFiles()) {

View file

@ -7,8 +7,12 @@ import type {
AgentAnalysisTracePayloadFullResult,
AgentAnalysisTracePayloadRequest,
AppConfig,
AppCaptureElementPngRequest,
AppCaptureElementPngResult,
AppDataExportResult,
AppInfo,
AppImageExportTargetRequest,
AppImageExportTargetResult,
AppSaveConfigOptions,
AppUpdateStatus,
ApiKeyConfig,
@ -86,6 +90,7 @@ contextBridge.exposeInMainWorld("ccr", {
applyClaudeAppGateway: (config?: AppConfig) => invoke(IPC_CHANNELS.appApplyClaudeAppGateway, config) as Promise<ClaudeAppGatewayApplyResult>,
applyProfile: () => invoke(IPC_CHANNELS.appApplyProfile) as Promise<ProfileApplyResult>,
cancelBotGatewayQrLogin: (request: BotGatewayQrLoginCancelRequest) => invoke(IPC_CHANNELS.appBotGatewayQrLoginCancel, request) as Promise<BotGatewayQrLoginCancelResult>,
captureElementPng: (request: AppCaptureElementPngRequest) => invoke(IPC_CHANNELS.appCaptureElementPng, request) as Promise<AppCaptureElementPngResult>,
checkProviderConnectivity: (request: GatewayProviderConnectivityCheckRequest) => invoke(IPC_CHANNELS.appCheckProviderConnectivity, request) as Promise<GatewayProviderConnectivityCheckReport>,
closeBotGatewayQrWindow: (request: BotGatewayQrWindowCloseRequest) => invoke(IPC_CHANNELS.appBotGatewayQrWindowClose, request) as Promise<BotGatewayQrWindowCloseResult>,
clearProxyNetworkCaptures: () => invoke(IPC_CHANNELS.appClearProxyNetworkCaptures) as Promise<ProxyNetworkSnapshot>,
@ -121,6 +126,7 @@ contextBridge.exposeInMainWorld("ccr", {
openBotGatewayQrWindow: (request: BotGatewayQrWindowOpenRequest) => invoke(IPC_CHANNELS.appBotGatewayQrWindowOpen, request) as Promise<BotGatewayQrWindowOpenResult>,
openExternal: (url: string) => invoke(IPC_CHANNELS.appOpenExternal, url) as Promise<void>,
openProfile: (request: ProfileOpenRequest) => invoke(IPC_CHANNELS.appOpenProfile, request) as Promise<ProfileOpenResult>,
prepareImageExportTarget: (request: AppImageExportTargetRequest) => invoke(IPC_CHANNELS.appPrepareImageExportTarget, request) as Promise<AppImageExportTargetResult>,
probeProviderCandidates: (request: GatewayProviderProbeCandidatesRequest) => invoke(IPC_CHANNELS.appProbeProviderCandidates, request) as Promise<GatewayProviderProbeCandidateResult | undefined>,
probeProvider: (request: GatewayProviderProbeRequest) => invoke(IPC_CHANNELS.appProbeProvider, request) as Promise<GatewayProviderProbeResult>,
quitApp: () => invoke(IPC_CHANNELS.appQuit) as Promise<void>,

View file

@ -76,7 +76,7 @@ const moonshotChinaProviderAccountConfig: ProviderAccountConfig = {
export const moonshotChinaProviderPreset: ProviderPreset = {
account: moonshotChinaProviderAccountConfig,
aliases: ["kimi", "kimi api", "moonshot", "moonshot kimi"],
defaultModels: ["moonshot-v1-8k"],
defaultModels: ["kimi-k2.7-code"],
endpoints: [
{
baseUrl: "https://api.moonshot.cn/v1",
@ -92,7 +92,7 @@ export const moonshotChinaProviderPreset: ProviderPreset = {
export const moonshotGlobalProviderPreset: ProviderPreset = {
account: moonshotGlobalProviderAccountConfig,
aliases: ["kimi", "kimi api", "moonshot", "moonshot kimi"],
defaultModels: ["moonshot-v1-8k"],
defaultModels: ["kimi-k2.7-code"],
endpoints: [
{
baseUrl: "https://api.moonshot.ai/v1",

View file

@ -22,6 +22,7 @@ import {
useState, X, XAxis, YAxis
} from "../shared";
import { buildTokenActivity, type TokenActivityCell } from "@/lib/usage-activity";
import { ShareCardWidget } from "./share-cards";
export function OverviewView({
onWidgetsChange,
overviewWidgets,
@ -223,6 +224,17 @@ export function OverviewView({
});
}
function changeWidgetShareData(id: string, type: ShareOverviewWidgetType) {
const current = widgets.find((widget) => widget.id === id);
if (!current) {
return;
}
updateWidget(id, {
type,
variant: overviewWidgetVariantOptions(type)[0]?.value ?? current.variant
});
}
function resetLayout() {
onWidgetsChange(DEFAULT_OVERVIEW_WIDGETS.map((widget) => ({ ...widget })));
setSelectedWidgetId(undefined);
@ -345,6 +357,7 @@ export function OverviewView({
onChangeBreakdownData={(type) => selectedWidget ? changeWidgetBreakdownData(selectedWidget.id, type) : undefined}
onChangeCategory={(category) => selectedWidget ? changeWidgetCategory(selectedWidget.id, category) : undefined}
onChangeMetric={(metric) => selectedWidget ? updateWidget(selectedWidget.id, { metric }) : undefined}
onChangeShareData={(type) => selectedWidget ? changeWidgetShareData(selectedWidget.id, type) : undefined}
onChangeSize={(size) => selectedWidget ? updateWidget(selectedWidget.id, { size }) : undefined}
onChangeVariant={(variant) => selectedWidget ? updateWidget(selectedWidget.id, { variant }) : undefined}
onRemove={() => selectedWidget ? removeWidget(selectedWidget.id) : undefined}
@ -354,6 +367,7 @@ export function OverviewView({
) : (
widgetGrid
)}
</motion.div>
);
}
@ -419,8 +433,8 @@ function OverviewWidgetPalette({
>
<Plus className="h-3.5 w-3.5 text-muted-foreground" />
<div className="min-w-0">
<div className="truncate text-[12px] font-semibold text-foreground">{t(overviewWidgetCategoryLabel(overviewWidgetCategory(template.type)))}</div>
<div className="mt-0.5 truncate text-[10px] text-muted-foreground">{t(overviewWidgetCategoryDescription(overviewWidgetCategory(template.type)))}</div>
<div className="truncate text-[12px] font-semibold text-foreground">{t(overviewWidgetPaletteTitle(template))}</div>
<div className="mt-0.5 truncate text-[10px] text-muted-foreground">{t(overviewWidgetPaletteDescription(template))}</div>
</div>
</Button>
))}
@ -436,6 +450,7 @@ function OverviewWidgetProperties({
onChangeBreakdownData,
onChangeCategory,
onChangeMetric,
onChangeShareData,
onChangeSize,
onChangeVariant,
onRemove
@ -447,6 +462,7 @@ function OverviewWidgetProperties({
onChangeBreakdownData: (type: "model-distribution" | "token-mix") => void;
onChangeCategory: (category: OverviewWidgetCategory) => void;
onChangeMetric: (metric: OverviewMetricKind) => void;
onChangeShareData: (type: ShareOverviewWidgetType) => void;
onChangeSize: (size: OverviewWidgetSize) => void;
onChangeVariant: (variant: OverviewWidgetVariant) => void;
onRemove: () => void;
@ -480,6 +496,9 @@ function OverviewWidgetProperties({
if (category === "breakdown") {
onChangeBreakdownData(value as "model-distribution" | "token-mix");
}
if (category === "share-card") {
onChangeShareData(value as ShareOverviewWidgetType);
}
};
return (
@ -847,6 +866,8 @@ function OverviewWidgetRenderer({
content = <ModelDistributionOverviewWidget dimensions={dimensions} rows={usageStats.models} variant={overviewTokenMixVariant(widget.variant)} />;
} else if (widget.type === "client-analysis") {
content = <OverviewAnalysisWidget dimensions={dimensions} kind="client" rows={usageStats.clientModels} variant={widget.variant === "compact" ? "compact" : "table"} />;
} else if (isShareOverviewWidgetType(widget.type)) {
content = <ShareCardWidget providerAccounts={providerAccounts} type={widget.type} usageRange={usageRange} usageStats={usageStats} />;
} else {
content = <OverviewAnalysisWidget dimensions={dimensions} kind="provider" rows={usageStats.providerModels} variant={widget.variant === "compact" ? "compact" : "table"} />;
}
@ -1464,11 +1485,19 @@ function overviewWidgetTemplates(): OverviewWidgetConfig[] {
{ enabled: true, id: "usage-trend", size: "3:2", type: "usage-trend", variant: "composed" },
{ enabled: true, id: "token-activity", size: "4:2", type: "token-activity", variant: "heatmap" },
{ 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: "client-analysis", size: "2:2", type: "client-analysis", variant: "table" },
{ enabled: true, id: "share-usage-wrapped", size: "1:4", type: "share-usage-wrapped", variant: "card" },
{ enabled: true, id: "share-route-map", size: "1:4", type: "share-route-map", variant: "card" },
{ enabled: true, id: "share-model-leaderboard", size: "1:4", type: "share-model-leaderboard", variant: "card" },
{ enabled: true, id: "share-fuel-cockpit", size: "1:4", type: "share-fuel-cockpit", variant: "card" },
{ enabled: true, id: "share-token-calendar", size: "1:4", type: "share-token-calendar", variant: "card" },
{ enabled: true, id: "share-spend-receipt", size: "1:4", type: "share-spend-receipt", variant: "card" }
];
}
type OverviewWidgetCategory = "account-balance" | "activity" | "analysis" | "breakdown" | "metric" | "system-status" | "usage-trend";
type ShareOverviewWidgetType = Extract<OverviewWidgetType, "share-fuel-cockpit" | "share-model-leaderboard" | "share-route-map" | "share-spend-receipt" | "share-token-calendar" | "share-usage-wrapped">;
type OverviewWidgetCategory = "account-balance" | "activity" | "analysis" | "breakdown" | "metric" | "share-card" | "system-status" | "usage-trend";
function overviewWidgetCategoryOptions(): Array<{ label: string; value: OverviewWidgetCategory }> {
return [
@ -1478,7 +1507,8 @@ function overviewWidgetCategoryOptions(): Array<{ label: string; value: Overview
"usage-trend",
"activity",
"breakdown",
"analysis"
"analysis",
"share-card"
].map((category) => ({
label: overviewWidgetCategoryLabel(category as OverviewWidgetCategory),
value: category as OverviewWidgetCategory
@ -1499,6 +1529,17 @@ function overviewBreakdownDataOptions(): Array<{ label: string; value: "model-di
];
}
function overviewShareCardDataOptions(): Array<{ label: string; value: ShareOverviewWidgetType }> {
return [
{ label: "AI Usage Wrapped", value: "share-usage-wrapped" },
{ label: "CCR Route Map", value: "share-route-map" },
{ label: "Model Leaderboard", value: "share-model-leaderboard" },
{ label: "AI Fuel Cockpit", value: "share-fuel-cockpit" },
{ label: "Token Calendar Poster", value: "share-token-calendar" },
{ label: "Spend Receipt", value: "share-spend-receipt" }
];
}
function overviewWidgetDataOptions(widget: OverviewWidgetConfig, providerAccounts: ProviderAccountSnapshot[]): Array<{ label: string; value: string }> {
const category = overviewWidgetCategory(widget.type);
if (category === "metric") {
@ -1526,6 +1567,9 @@ function overviewWidgetDataOptions(widget: OverviewWidgetConfig, providerAccount
if (category === "breakdown") {
return overviewBreakdownDataOptions();
}
if (category === "share-card") {
return overviewShareCardDataOptions();
}
return [{ label: "Usage over time", value: "usage-trend" }];
}
@ -1546,6 +1590,9 @@ function overviewWidgetDataValue(widget: OverviewWidgetConfig): string {
if (category === "activity") {
return "token-activity";
}
if (category === "share-card") {
return widget.type;
}
return category;
}
@ -1559,6 +1606,9 @@ function overviewWidgetCategory(type: OverviewWidgetType): OverviewWidgetCategor
if (type === "token-activity") {
return "activity";
}
if (isShareOverviewWidgetType(type)) {
return "share-card";
}
return type;
}
@ -1572,10 +1622,16 @@ function overviewWidgetTypeForCategory(category: OverviewWidgetCategory, current
if (category === "activity") {
return "token-activity";
}
if (category === "share-card") {
return isShareOverviewWidgetType(currentType) ? currentType : "share-usage-wrapped";
}
return category;
}
function overviewWidgetTemplateKey(widget: OverviewWidgetConfig): string {
if (isShareOverviewWidgetType(widget.type)) {
return widget.type;
}
return overviewWidgetCategory(widget.type);
}
@ -1584,6 +1640,7 @@ function overviewWidgetCategoryLabel(category: OverviewWidgetCategory): string {
if (category === "analysis") return "Analysis component";
if (category === "activity") return "Activity component";
if (category === "metric") return "Metric component";
if (category === "share-card") return "Share card";
if (category === "system-status") return "Status component";
if (category === "breakdown") return "Breakdown component";
return "Trend component";
@ -1594,11 +1651,24 @@ function overviewWidgetCategoryDescription(category: OverviewWidgetCategory): st
if (category === "analysis") return "Client or provider";
if (category === "activity") return "Token activity heatmap";
if (category === "metric") return "Requests, tokens, cost";
if (category === "share-card") return "Social media PNG cards";
if (category === "system-status") return "Status timeline";
if (category === "breakdown") return "Token or model distribution";
return "Usage over time";
}
function overviewWidgetPaletteTitle(widget: OverviewWidgetConfig): string {
return isShareOverviewWidgetType(widget.type)
? overviewWidgetTypeLabel(widget.type)
: overviewWidgetCategoryLabel(overviewWidgetCategory(widget.type));
}
function overviewWidgetPaletteDescription(widget: OverviewWidgetConfig): string {
return isShareOverviewWidgetType(widget.type)
? overviewWidgetCategoryLabel("share-card")
: overviewWidgetCategoryDescription(overviewWidgetCategory(widget.type));
}
function overviewWidgetTitle(widget: OverviewWidgetConfig, translate: (value: string) => string): string {
if (widget.type === "metric") {
return translate(overviewMetricLabel(widget.metric ?? "requests"));
@ -1612,6 +1682,12 @@ function overviewWidgetTypeLabel(type: OverviewWidgetType): string {
if (type === "metric") return "Metric";
if (type === "model-distribution") return "Model Distribution";
if (type === "provider-analysis") return "Provider Analysis";
if (type === "share-fuel-cockpit") return "AI Fuel Cockpit";
if (type === "share-model-leaderboard") return "Model Leaderboard";
if (type === "share-route-map") return "CCR Route Map";
if (type === "share-spend-receipt") return "Spend Receipt";
if (type === "share-token-calendar") return "Token Calendar Poster";
if (type === "share-usage-wrapped") return "AI Usage Wrapped";
if (type === "system-status") return "System status";
if (type === "token-activity") return "Activity";
if (type === "token-mix") return "Token Mix";
@ -1665,12 +1741,26 @@ function overviewWidgetVariantOptions(type: OverviewWidgetType): Array<{ label:
{ label: "Compact", value: "compact" }
];
}
if (isShareOverviewWidgetType(type)) {
return [
{ label: "Card", value: "card" }
];
}
return [
{ label: "Table", value: "table" },
{ label: "Compact", value: "compact" }
];
}
function isShareOverviewWidgetType(type: OverviewWidgetType): type is ShareOverviewWidgetType {
return type === "share-fuel-cockpit" ||
type === "share-model-leaderboard" ||
type === "share-route-map" ||
type === "share-spend-receipt" ||
type === "share-token-calendar" ||
type === "share-usage-wrapped";
}
function overviewWidgetSizeClass(size: OverviewWidgetSize): string {
const { height, width } = overviewWidgetDimensions(size);
return cn(overviewWidgetWidthClass(width), overviewWidgetHeightClass(height));

View file

@ -2,7 +2,7 @@ import {
AddProviderDraft, AnimatedDisclosure, AnimatedIconSwap, AnimatedListItem, AnimatedPopover, AnimatePresence, AppConfig, Badge,
Box, Braces, Button, Card, CardContent, CardHeader, CardTitle,
Check, Checkbox, ChevronDown, ChevronRight, CircleAlert, cn,
compareProviderAccountSnapshots, Copy, copyTextToClipboard, createDefaultProviderAccountDraft, createModelCatalogItems, createProviderAccountDraftFromConfig, createProviderCredentialDraft, createProviderInstallLinkFromDraft,
compareProviderAccountSnapshots, copyTextToClipboard, createDefaultProviderAccountDraft, createModelCatalogItems, createProviderAccountDraftFromConfig, createProviderCredentialDraft,
customProviderPresetId, defaultProviderAccountConfigForPreset, Dialog, DialogBody, DialogContent, DialogFooter,
DialogHeader, DialogTitle, ExternalLink, Field, findProviderPreset, formatProviderAccountMeterValue, GatewayProviderConfig,
GatewayProviderProbeResult, getProviderPresets, Globe, inferProviderNameFromBaseUrl, Input, KeyValueRowsControl, Label,
@ -624,7 +624,7 @@ function ProviderPresetCombobox({
const [query, setQuery] = useState("");
const rootRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const selected = options.find((option) => option.value === value) ?? options[0];
const selected = options.find((option) => option.value === value) ?? options.find((option) => option.value === "");
const normalizedQuery = query.trim().toLowerCase();
const filteredOptions = normalizedQuery
? options.filter((option) => providerPresetOptionMatchesQuery(option, normalizedQuery))
@ -1232,9 +1232,9 @@ export function AddProviderForm({
const safetyIssue = providerDraftSafetyIssue(draft, detectedBaseUrl);
const localAgentImport = draft.providerPlugins.length > 0;
const providerPresetOptions = [
{ iconUrl: draft.icon, label: t("Other / custom API endpoint"), value: customProviderPresetId },
{ label: t("Select preset provider"), value: "" },
...getProviderPresets().map((preset) => ({ label: t(preset.name), preset, value: preset.id })),
{ iconUrl: draft.icon, label: t("Other / custom API endpoint"), value: customProviderPresetId }
...getProviderPresets().map((preset) => ({ label: t(preset.name), preset, value: preset.id }))
];
const selectableProtocols = providerSelectableProtocolsFromProbe(probe);
const hasConnectivityCheckInputs = Boolean(
@ -1800,7 +1800,6 @@ function ProviderUsageSettings({
const [testLoading, setTestLoading] = useState(false);
const [testResult, setTestResult] = useState<ProviderAccountTestResult>();
const [testError, setTestError] = useState("");
const [copiedLink, setCopiedLink] = useState(false);
const modeOptions = translateOptions(providerAccountModeOptions, t);
useEffect(() => {
@ -1808,14 +1807,6 @@ function ProviderUsageSettings({
setTestError("");
}, [draft.accountMode, draft.usageRequestUrl, draft.usageRequestMethod]);
useEffect(() => {
if (!copiedLink) {
return;
}
const timer = window.setTimeout(() => setCopiedLink(false), 1300);
return () => window.clearTimeout(timer);
}, [copiedLink]);
async function testUsageRequest() {
if (!window.ccr?.testProviderAccountConnector) {
setTestError(t("Request failed."));
@ -1855,17 +1846,6 @@ function ProviderUsageSettings({
}
}
async function copyProviderPluginLink() {
const link = createProviderInstallLinkFromDraft(draft, probe);
if (typeof link === "string" && link.startsWith("ccr://")) {
await copyTextToClipboard(link);
setCopiedLink(true);
setTestError("");
return;
}
setTestError(formatError(new Error(link)));
}
function selectPath(target: ProviderUsageFieldTarget, path: string) {
onChange(providerUsageFieldPatch(target, path));
}
@ -1880,12 +1860,6 @@ function ProviderUsageSettings({
/>
<span className="min-w-0 truncate">{t("Fetch usage")}</span>
</Label>
<Button className="h-8 shrink-0 px-2" onClick={() => void copyProviderPluginLink()} type="button" variant="outline">
<AnimatedIconSwap iconKey={copiedLink ? "copied" : "copy"}>
{copiedLink ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
</AnimatedIconSwap>
{copiedLink ? t("Copied") : t("Copy provider plugin link")}
</Button>
</div>
{draft.accountEnabled ? (

View file

@ -0,0 +1,775 @@
import {
Button, cn, Download, formatCompactNumber, formatDuration, formatPercent, formatProviderAccountMeterTitle,
formatProviderAccountMeterValue, formatUsdCost, LoaderCircle, primaryProviderAccountMeter, providerAccountMeterRemainingRatio,
providerAccountSnapshotKey, ReactNode, UsageStatsRange, UsageStatsSnapshot, useRef, useState,
useAppText, type OverviewWidgetType, type ProviderAccountMeter, type ProviderAccountSnapshot
} from "../shared";
import { buildTokenActivity, type TokenActivityCell } from "@/lib/usage-activity";
type ShareCardTone = "amber" | "blue" | "emerald" | "indigo" | "rose" | "slate" | "teal";
type ShareCardWidgetProps = {
providerAccounts: ProviderAccountSnapshot[];
type: OverviewWidgetType;
usageRange: UsageStatsRange;
usageStats: UsageStatsSnapshot;
};
type ShareCardPngExportOptions = {
exportId?: string;
output?: {
height: number;
width: number;
};
};
type ShareCardPreparedExportTarget = {
canceled: boolean;
exportId?: string;
};
const shareCardExportCssWidth = 540;
const shareCardExportCssHeight = 960;
const shareCardExportPixelWidth = 1080;
const shareCardExportPixelHeight = 1920;
const shareCardTones: Record<ShareCardTone, { accent: string; background: string; border: string; glow: string; muted: string; text: string }> = {
amber: {
accent: "#f59e0b",
background: "linear-gradient(135deg, #fffbeb 0%, #fef3c7 42%, #fff7ed 100%)",
border: "border-amber-200",
glow: "bg-amber-300/28",
muted: "text-amber-900/62",
text: "text-amber-950"
},
blue: {
accent: "#2563eb",
background: "linear-gradient(135deg, #eff6ff 0%, #dbeafe 44%, #ecfeff 100%)",
border: "border-blue-200",
glow: "bg-blue-300/30",
muted: "text-blue-950/62",
text: "text-blue-950"
},
emerald: {
accent: "#059669",
background: "linear-gradient(135deg, #ecfdf5 0%, #d1fae5 42%, #f0fdfa 100%)",
border: "border-emerald-200",
glow: "bg-emerald-300/28",
muted: "text-emerald-950/62",
text: "text-emerald-950"
},
indigo: {
accent: "#4f46e5",
background: "linear-gradient(135deg, #eef2ff 0%, #e0e7ff 46%, #f5f3ff 100%)",
border: "border-indigo-200",
glow: "bg-indigo-300/30",
muted: "text-indigo-950/62",
text: "text-indigo-950"
},
rose: {
accent: "#e11d48",
background: "linear-gradient(135deg, #fff1f2 0%, #ffe4e6 45%, #fff7ed 100%)",
border: "border-rose-200",
glow: "bg-rose-300/28",
muted: "text-rose-950/62",
text: "text-rose-950"
},
slate: {
accent: "#475569",
background: "linear-gradient(135deg, #f8fafc 0%, #e2e8f0 48%, #f1f5f9 100%)",
border: "border-slate-200",
glow: "bg-slate-300/32",
muted: "text-slate-700",
text: "text-slate-950"
},
teal: {
accent: "#0f766e",
background: "linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 42%, #ecfeff 100%)",
border: "border-teal-200",
glow: "bg-teal-300/30",
muted: "text-teal-950/62",
text: "text-teal-950"
}
};
export function ShareCardWidget({
providerAccounts,
type,
usageRange,
usageStats
}: ShareCardWidgetProps) {
const t = useAppText();
const definition = shareCardDefinition(type);
if (!definition) {
return null;
}
return (
<ShareCardShell fileName={definition.fileName} title={t(definition.title)}>
{type === "share-usage-wrapped" ? <UsageWrappedCard usageStats={usageStats} /> : null}
{type === "share-route-map" ? <RouteMapCard usageStats={usageStats} /> : null}
{type === "share-model-leaderboard" ? <ModelLeaderboardCard usageStats={usageStats} /> : null}
{type === "share-fuel-cockpit" ? <FuelCockpitCard providerAccounts={providerAccounts} /> : null}
{type === "share-token-calendar" ? <TokenCalendarPosterCard usageStats={usageStats} /> : null}
{type === "share-spend-receipt" ? <SpendReceiptCard usageRange={usageRange} usageStats={usageStats} /> : null}
</ShareCardShell>
);
}
function shareCardDefinition(type: OverviewWidgetType): { fileName: string; title: string } | undefined {
if (type === "share-usage-wrapped") return { fileName: "ccr-ai-usage-wrapped.png", title: "AI Usage Wrapped" };
if (type === "share-route-map") return { fileName: "ccr-route-map.png", title: "CCR Route Map" };
if (type === "share-model-leaderboard") return { fileName: "ccr-model-leaderboard.png", title: "Model Leaderboard" };
if (type === "share-fuel-cockpit") return { fileName: "ccr-ai-fuel-cockpit.png", title: "AI Fuel Cockpit" };
if (type === "share-token-calendar") return { fileName: "ccr-token-calendar-poster.png", title: "Token Calendar Poster" };
if (type === "share-spend-receipt") return { fileName: "ccr-spend-receipt.png", title: "Spend Receipt" };
return undefined;
}
function ShareCardShell({
children,
fileName,
title
}: {
children: ReactNode;
fileName: string;
title: string;
}) {
const t = useAppText();
const cardRef = useRef<HTMLDivElement>(null);
const exportCardRef = useRef<HTMLDivElement>(null);
const [exporting, setExporting] = useState(false);
const [status, setStatus] = useState<"error" | "idle" | "saving">("idle");
async function saveImage() {
if (status === "saving") {
return;
}
setStatus("saving");
let exportTarget: ShareCardPreparedExportTarget | undefined;
try {
if (window.ccr?.prepareImageExportTarget) {
exportTarget = await window.ccr.prepareImageExportTarget({ fileName });
if (exportTarget.canceled) {
setStatus("idle");
return;
}
}
setExporting(true);
await nextFrame();
await nextFrame();
const target = exportCardRef.current ?? cardRef.current;
if (!target) {
throw new Error("Export card is unavailable.");
}
await saveElementAsPng(target, fileName, {
exportId: exportTarget?.exportId,
output: {
height: shareCardExportPixelHeight,
width: shareCardExportPixelWidth
}
});
setStatus("idle");
} catch (error) {
console.error(error);
setStatus("error");
} finally {
setExporting(false);
}
}
return (
<article className="flex h-full min-h-0 min-w-0 flex-col space-y-2">
<div className="flex min-w-0 items-center justify-between gap-2">
<div className="min-w-0 truncate text-[12px] font-semibold text-muted-foreground">{title}</div>
<div className="flex shrink-0 items-center gap-2">
{status === "error" ? <span className="text-[11px] font-medium text-destructive">{t("Export failed")}</span> : null}
<Button disabled={status === "saving"} size="sm" type="button" variant="outline" onClick={() => void saveImage()}>
{status === "saving" ? <LoaderCircle className="h-3.5 w-3.5 animate-spin" /> : <Download className="h-3.5 w-3.5" />}
{status === "saving" ? t("Saving") : t("Save image")}
</Button>
</div>
</div>
<div className="min-h-0 flex-1" ref={cardRef}>
{children}
</div>
{exporting ? (
<div
aria-hidden="true"
className="pointer-events-none fixed left-0 top-0 z-[9999]"
ref={exportCardRef}
style={{
height: `${shareCardExportCssHeight}px`,
width: `${shareCardExportCssWidth}px`
}}
>
{children}
</div>
) : null}
</article>
);
}
function UsageWrappedCard({ usageStats }: { usageStats: UsageStatsSnapshot }) {
const t = useAppText();
const totals = usageStats.totals;
const topModel = usageStats.models[0];
const topProvider = usageStats.providerModels[0];
const bestDay = [...usageStats.series].sort((a, b) => b.totalTokens - a.totalTokens)[0];
const activity = buildTokenActivity(usageStats.series, { maxWeeks: 12, minWeeks: 8 });
return (
<SharePoster tone="teal">
<SharePosterHeader title={t("AI Usage Wrapped")} tone="teal" />
<div className="mt-8">
<div className="text-[60px] font-black leading-none tracking-normal text-teal-950">{formatCompactNumber(totals.totalTokens)}</div>
<div className="mt-2 text-[17px] font-semibold text-teal-950/70">{t("tokens routed through CCR")}</div>
</div>
<div className="mt-8 grid grid-cols-2 gap-3">
<PosterStat label={t("Requests")} value={formatCompactNumber(totals.requestCount)} />
<PosterStat label={t("Estimated cost")} value={formatUsdCost(totals.costUsd)} />
<PosterStat label={t("Cache ratio")} value={formatPercent(totals.cacheRatio)} />
<PosterStat label={t("Longest streak")} value={`${formatCompactNumber(activity.longestStreak)} ${t(activity.longestStreak === 1 ? "day" : "days")}`} />
</div>
<div className="mt-8 grid gap-3">
<PosterHighlight label={t("Top model")} value={topModel?.label ?? t("No model activity")} />
<PosterHighlight label={t("Top provider")} value={topProvider?.provider || topProvider?.label || t("No provider activity")} />
<PosterHighlight label={t("Peak day")} value={bestDay ? `${bestDay.label} / ${formatCompactNumber(bestDay.totalTokens)}` : "-"} />
</div>
<SharePosterFooter tone="teal" />
</SharePoster>
);
}
function RouteMapCard({ usageStats }: { usageStats: UsageStatsSnapshot }) {
const t = useAppText();
const routes = buildRouteRows(usageStats, t);
const total = routes.reduce((sum, row) => sum + row.value, 0) || 1;
return (
<SharePoster tone="indigo">
<SharePosterHeader title={t("CCR Route Map")} tone="indigo" />
<div className="mt-7 rounded-[22px] border border-indigo-200/80 bg-white/62 p-4 shadow-[0_16px_34px_rgba(79,70,229,0.10)]">
<div className="grid grid-cols-[1fr_64px_1fr] items-center gap-3 text-center text-[10px] font-bold uppercase text-indigo-950/55">
<span>{t("Client")}</span>
<span>CCR</span>
<span>{t("Model")}</span>
</div>
<div className="mt-4 space-y-3">
{routes.length > 0 ? routes.slice(0, 5).map((route, index) => {
const share = Math.max(8, (route.value / total) * 100);
return (
<div className="grid grid-cols-[1fr_64px_1fr] items-center gap-3" key={`${route.client}-${route.provider}-${route.model}-${index}`}>
<RoutePill label={route.client} />
<div className="relative flex h-9 items-center justify-center">
<span className="absolute left-0 right-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-indigo-200" />
<span className="absolute left-0 top-1/2 h-2 -translate-y-1/2 rounded-full bg-indigo-500" style={{ width: `${share}%` }} />
<span className="relative flex h-9 w-9 items-center justify-center rounded-full bg-indigo-600 text-[10px] font-black text-white shadow-[0_8px_18px_rgba(79,70,229,0.32)]">{index + 1}</span>
</div>
<RoutePill label={`${route.provider} / ${route.model}`} align="right" />
</div>
);
}) : (
<div className="rounded-[18px] border border-dashed border-indigo-300 bg-indigo-50/60 px-4 py-12 text-center text-[14px] font-semibold text-indigo-950/55">
{t("No route activity")}
</div>
)}
</div>
</div>
<div className="mt-6 grid grid-cols-3 gap-2">
<MiniRouteMetric label={t("Clients")} value={formatCompactNumber(uniqueCount(routes.map((row) => row.client)))} />
<MiniRouteMetric label={t("Providers")} value={formatCompactNumber(uniqueCount(routes.map((row) => row.provider)))} />
<MiniRouteMetric label={t("Models")} value={formatCompactNumber(uniqueCount(routes.map((row) => row.model)))} />
</div>
<SharePosterFooter tone="indigo" />
</SharePoster>
);
}
function ModelLeaderboardCard({ usageStats }: { usageStats: UsageStatsSnapshot }) {
const t = useAppText();
const rows = usageStats.models.filter((row) => row.totalTokens > 0).slice(0, 5);
const max = Math.max(...rows.map((row) => row.totalTokens), 1);
return (
<SharePoster tone="blue">
<SharePosterHeader title={t("Model Leaderboard")} tone="blue" />
<div className="mt-7 space-y-3">
{rows.length > 0 ? rows.map((row, index) => (
<div className="rounded-[20px] border border-blue-200/80 bg-white/66 p-3 shadow-[0_12px_28px_rgba(37,99,235,0.10)]" key={row.key}>
<div className="flex min-w-0 items-center gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[14px] bg-blue-600 text-[17px] font-black text-white">{index + 1}</span>
<div className="min-w-0 flex-1">
<div className="truncate text-[15px] font-black text-blue-950">{row.label}</div>
<div className="mt-0.5 truncate text-[11px] font-semibold text-blue-950/55">{row.provider ?? t("All providers")}</div>
</div>
<div className="shrink-0 text-right">
<div className="text-[16px] font-black text-blue-950">{formatPercent(row.maxShare)}</div>
<div className="text-[10px] font-semibold text-blue-950/52">{formatCompactNumber(row.totalTokens)}</div>
</div>
</div>
<div className="mt-3 h-2 overflow-hidden rounded-full bg-blue-100">
<div className="h-full rounded-full bg-blue-600" style={{ width: `${Math.max(4, (row.totalTokens / max) * 100)}%` }} />
</div>
</div>
)) : (
<EmptyPosterState label={t("No model activity")} tone="blue" />
)}
</div>
<SharePosterFooter tone="blue" />
</SharePoster>
);
}
function FuelCockpitCard({ providerAccounts }: { providerAccounts: ProviderAccountSnapshot[] }) {
const t = useAppText();
const accountRows = providerAccounts
.filter((account) => account.meters.length > 0)
.map((account) => ({
account,
meter: fuelMeter(account)
}))
.filter((row): row is { account: ProviderAccountSnapshot; meter: ProviderAccountMeter } => Boolean(row.meter))
.slice(0, 3);
return (
<SharePoster tone="emerald">
<SharePosterHeader title={t("AI Fuel Cockpit")} tone="emerald" />
<div className="mt-7 grid gap-4">
{accountRows.length > 0 ? accountRows.map(({ account, meter }, index) => {
const ratio = providerAccountMeterRemainingRatio(meter) ?? (meter.kind === "balance" ? 1 : 0);
return (
<div className="grid grid-cols-[112px_minmax(0,1fr)] items-center gap-4 rounded-[22px] border border-emerald-200/80 bg-white/66 p-4 shadow-[0_14px_30px_rgba(5,150,105,0.10)]" key={providerAccountSnapshotKey(account)}>
<CircularGauge color={index === 0 ? "#059669" : index === 1 ? "#2563eb" : "#d97706"} ratio={ratio} value={formatPercent(ratio)} />
<div className="min-w-0">
<div className="truncate text-[16px] font-black text-emerald-950">{safeProviderName(account.provider, t)}</div>
<div className="mt-1 truncate text-[12px] font-semibold text-emerald-950/58">{formatProviderAccountMeterTitle(meter, t)}</div>
<div className="mt-3 text-[26px] font-black leading-none text-emerald-950">{formatProviderAccountMeterValue(meter)}</div>
</div>
</div>
);
}) : (
<EmptyPosterState label={t("No account balance connectors configured")} tone="emerald" />
)}
</div>
<SharePosterFooter tone="emerald" />
</SharePoster>
);
}
function TokenCalendarPosterCard({ usageStats }: { usageStats: UsageStatsSnapshot }) {
const t = useAppText();
const activity = buildTokenActivity(usageStats.series, { maxWeeks: 26, minWeeks: 18 });
return (
<SharePoster tone="rose">
<SharePosterHeader title={t("Token Calendar Poster")} tone="rose" />
<div className="mt-7 rounded-[22px] border border-rose-200/80 bg-white/66 p-4 shadow-[0_14px_30px_rgba(225,29,72,0.10)]">
<div
className="grid"
style={{
gap: "5px",
gridTemplateColumns: `repeat(${activity.weekCount}, minmax(0, 1fr))`,
gridTemplateRows: "repeat(7, minmax(0, 1fr))"
}}
>
{activity.cells.map((cell) => (
<span
className="aspect-square rounded-[5px]"
key={cell.dateKey}
style={{
backgroundColor: calendarColor(cell.intensity, cell.inObservedRange),
gridColumn: cell.weekIndex + 1,
gridRow: cell.dayIndex + 1
}}
/>
))}
</div>
</div>
<div className="mt-5 grid grid-cols-3 gap-2">
<PosterStat label={t("Longest streak")} value={`${formatCompactNumber(activity.longestStreak)}d`} />
<PosterStat label={t("Avg / day")} value={formatCompactNumber(Math.round(activity.avgPerDay))} />
<PosterStat label={t("Total")} value={formatCompactNumber(activity.totalTokens)} />
</div>
<div className="mt-5 flex items-center gap-2 text-[12px] font-bold text-rose-950/62">
<span>{t("Less")}</span>
{[0, 1, 2, 3, 4].map((intensity) => (
<span
className="h-4 w-4 rounded-[5px]"
key={intensity}
style={{ backgroundColor: calendarColor(intensity as TokenActivityCell["intensity"], true) }}
/>
))}
<span>{t("More")}</span>
</div>
<SharePosterFooter tone="rose" />
</SharePoster>
);
}
function SpendReceiptCard({
usageRange,
usageStats
}: {
usageRange: UsageStatsRange;
usageStats: UsageStatsSnapshot;
}) {
const t = useAppText();
const totals = usageStats.totals;
const rows = [
{ label: t("Range"), value: rangeLabel(usageRange, t) },
{ label: t("Requests"), value: formatCompactNumber(totals.requestCount) },
{ label: t("Input tokens"), value: formatCompactNumber(totals.inputTokens) },
{ label: t("Output tokens"), value: formatCompactNumber(totals.outputTokens) },
{ label: t("Cache tokens"), value: formatCompactNumber(totals.cacheTokens) },
{ label: t("Average latency"), value: formatDuration(totals.avgDurationMs) },
{ label: t("Success rate"), value: formatPercent(totals.successRate) }
];
return (
<SharePoster tone="slate">
<SharePosterHeader title={t("Spend Receipt")} tone="slate" />
<div className="mt-7 rounded-[22px] border border-slate-300 bg-white/74 p-5 shadow-[0_16px_34px_rgba(71,85,105,0.12)]">
<div className="border-b border-dashed border-slate-300 pb-4 text-center">
<div className="text-[40px] font-black leading-none text-slate-950">{formatUsdCost(totals.costUsd)}</div>
<div className="mt-2 text-[12px] font-bold uppercase text-slate-500">{t("Estimated cost")}</div>
</div>
<div className="mt-4 space-y-2 border-b border-dashed border-slate-300 pb-4">
{rows.map((row) => (
<div className="flex min-w-0 items-center justify-between gap-4 text-[13px]" key={row.label}>
<span className="truncate font-semibold text-slate-500">{row.label}</span>
<span className="shrink-0 font-black text-slate-950">{row.value}</span>
</div>
))}
</div>
<div className="mt-4 flex items-center justify-between gap-4">
<div className="text-[11px] font-bold uppercase text-slate-500">{t("Total tokens")}</div>
<div className="text-[30px] font-black leading-none text-slate-950">{formatCompactNumber(totals.totalTokens)}</div>
</div>
</div>
<SharePosterFooter tone="slate" />
</SharePoster>
);
}
function SharePoster({
children,
tone
}: {
children: ReactNode;
tone: ShareCardTone;
}) {
const theme = shareCardTones[tone];
return (
<div
className={cn("relative h-full min-h-0 w-full overflow-hidden rounded-[28px] border p-6 shadow-card-elevated", theme.border, theme.text)}
style={{ background: theme.background }}
>
<span className={cn("absolute -right-16 -top-16 h-44 w-44 rounded-full blur-3xl", theme.glow)} />
<span className={cn("absolute -bottom-20 left-8 h-44 w-44 rounded-full blur-3xl", theme.glow)} />
<div className="relative z-10 flex h-full min-h-0 flex-col">
{children}
</div>
</div>
);
}
function SharePosterHeader({
title,
tone
}: {
title: string;
tone: ShareCardTone;
}) {
const theme = shareCardTones[tone];
return (
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<h4 className="text-[30px] font-black leading-[1.02] tracking-normal">{title}</h4>
</div>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-[18px] border border-white/70 bg-white/58 shadow-[0_12px_28px_rgba(15,23,42,0.10)]">
<span className="text-[16px] font-black" style={{ color: theme.accent }}>CCR</span>
</div>
</div>
);
}
function SharePosterFooter({ tone }: { tone: ShareCardTone }) {
const t = useAppText();
const theme = shareCardTones[tone];
return (
<div className={cn("mt-auto flex min-w-0 items-center justify-between gap-3 pt-5 text-[11px] font-black uppercase tracking-[0.12em]", theme.muted)}>
<span>{t("Generated by CCR")}</span>
<span>{new Date().getFullYear()}</span>
</div>
);
}
function PosterStat({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0 rounded-[18px] border border-white/70 bg-white/54 px-3 py-3 shadow-[0_10px_24px_rgba(15,23,42,0.08)]">
<div className="truncate text-[11px] font-bold text-slate-600">{label}</div>
<div className="mt-1 truncate text-[22px] font-black leading-none text-slate-950">{value}</div>
</div>
);
}
function PosterHighlight({ label, value }: { label: string; value: string }) {
return (
<div className="flex min-w-0 items-center justify-between gap-3 rounded-[18px] border border-white/70 bg-white/48 px-4 py-3">
<span className="truncate text-[12px] font-bold text-slate-600">{label}</span>
<span className="min-w-0 truncate text-right text-[14px] font-black text-slate-950">{value}</span>
</div>
);
}
function EmptyPosterState({ label, tone }: { label: string; tone: ShareCardTone }) {
const theme = shareCardTones[tone];
return (
<div className={cn("rounded-[22px] border border-dashed bg-white/52 px-4 py-16 text-center text-[14px] font-bold", theme.border, theme.muted)}>
{label}
</div>
);
}
function RoutePill({ align = "left", label }: { align?: "left" | "right"; label: string }) {
return (
<div className={cn("min-w-0 rounded-[16px] border border-indigo-200 bg-white/72 px-3 py-2 shadow-[0_8px_18px_rgba(79,70,229,0.08)]", align === "right" && "text-right")}>
<div className="truncate text-[12px] font-black text-indigo-950">{label}</div>
</div>
);
}
function MiniRouteMetric({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-[16px] border border-indigo-200/80 bg-white/58 px-3 py-2 text-center">
<div className="text-[20px] font-black leading-none text-indigo-950">{value}</div>
<div className="mt-1 truncate text-[10px] font-bold uppercase text-indigo-950/50">{label}</div>
</div>
);
}
function CircularGauge({ color, ratio, value }: { color: string; ratio: number; value: string }) {
const radius = 41;
const circumference = 2 * Math.PI * radius;
const clamped = Math.max(0, Math.min(1, ratio));
return (
<svg className="h-[112px] w-[112px]" role="img" viewBox="0 0 112 112" aria-label={value}>
<circle cx="56" cy="56" fill="rgba(255,255,255,.55)" r="48" />
<circle cx="56" cy="56" fill="none" r={radius} stroke="rgba(15,23,42,.10)" strokeWidth="10" />
<circle
cx="56"
cy="56"
fill="none"
r={radius}
stroke={color}
strokeDasharray={circumference}
strokeDashoffset={circumference * (1 - clamped)}
strokeLinecap="round"
strokeWidth="10"
transform="rotate(-90 56 56)"
/>
<text fill="#064e3b" fontSize="18" fontWeight="900" textAnchor="middle" x="56" y="61">{value}</text>
</svg>
);
}
function buildRouteRows(usageStats: UsageStatsSnapshot, translate: (value: string) => string): Array<{ client: string; model: string; provider: string; value: number }> {
const rows = usageStats.clientModels
.filter((row) => row.totalTokens > 0)
.map((row) => ({
client: row.client || row.label || translate("Agent"),
model: row.model || modelNameFromLabel(row.label),
provider: row.provider || translate("Provider"),
value: row.totalTokens
}));
if (rows.length > 0) {
return rows.sort((a, b) => b.value - a.value);
}
return usageStats.models
.filter((row) => row.totalTokens > 0)
.map((row) => ({
client: translate("Agent"),
model: row.model || row.label,
provider: row.provider || translate("Provider"),
value: row.totalTokens
}))
.sort((a, b) => b.value - a.value);
}
function modelNameFromLabel(label: string): string {
const parts = label.split("/");
return parts[parts.length - 1]?.trim() || label;
}
function fuelMeter(account: ProviderAccountSnapshot): ProviderAccountMeter | undefined {
return [...account.meters]
.filter((meter) => providerAccountMeterRemainingRatio(meter) !== undefined)
.sort((a, b) => (providerAccountMeterRemainingRatio(a) ?? 1) - (providerAccountMeterRemainingRatio(b) ?? 1))[0] ?? primaryProviderAccountMeter(account);
}
function safeProviderName(value: string, translate: (value: string) => string): string {
return value.trim() || translate("Provider");
}
function uniqueCount(values: string[]): number {
return new Set(values.filter(Boolean)).size;
}
function rangeLabel(range: UsageStatsRange, translate: (value: string) => string): string {
if (range === "today") {
return translate("Today");
}
return translate(range);
}
function calendarColor(intensity: TokenActivityCell["intensity"], inRange: boolean): string {
if (!inRange) return "rgba(225,29,72,.08)";
if (intensity === 0) return "rgba(225,29,72,.13)";
if (intensity === 1) return "rgba(225,29,72,.30)";
if (intensity === 2) return "rgba(225,29,72,.50)";
if (intensity === 3) return "rgba(225,29,72,.72)";
return "rgba(225,29,72,.94)";
}
async function saveElementAsPng(element: HTMLElement, fileName: string, options: ShareCardPngExportOptions = {}): Promise<void> {
if (window.ccr?.captureElementPng) {
await nativeCaptureElementAsPng(element, fileName, options);
return;
}
await browserExportElementAsPng(element, fileName);
}
async function nativeCaptureElementAsPng(element: HTMLElement, fileName: string, options: ShareCardPngExportOptions): Promise<void> {
element.scrollIntoView({ block: "nearest", inline: "nearest" });
await nextFrame();
const rect = element.getBoundingClientRect();
const result = await window.ccr?.captureElementPng?.({
borderRadius: exportBorderRadius(element),
exportId: options.exportId,
fileName,
output: options.output,
rect: {
height: rect.height,
width: rect.width,
x: rect.x,
y: rect.y
}
});
if (!result || result.canceled) {
return;
}
}
function exportBorderRadius(element: HTMLElement): number | undefined {
const target = element.firstElementChild instanceof HTMLElement ? element.firstElementChild : element;
const styles = window.getComputedStyle(target);
const radii = [
styles.borderTopLeftRadius,
styles.borderTopRightRadius,
styles.borderBottomRightRadius,
styles.borderBottomLeftRadius
].map((value) => Number.parseFloat(value)).filter(Number.isFinite);
const radius = Math.max(0, ...radii);
return radius > 0 ? radius : undefined;
}
function nextFrame(): Promise<void> {
return new Promise((resolve) => {
window.requestAnimationFrame(() => resolve());
});
}
async function browserExportElementAsPng(element: HTMLElement, fileName: string): Promise<void> {
const rect = element.getBoundingClientRect();
const width = Math.ceil(rect.width);
const height = Math.ceil(rect.height);
if (width <= 0 || height <= 0) {
throw new Error("Cannot export an empty element.");
}
const clone = element.cloneNode(true) as HTMLElement;
clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
clone.style.width = `${width}px`;
clone.style.height = `${height}px`;
inlineComputedStyles(element, clone);
const serialized = new XMLSerializer().serializeToString(clone);
const svg = [
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
`<foreignObject width="100%" height="100%">${serialized}</foreignObject>`,
"</svg>"
].join("");
const svgUrl = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
try {
const image = await loadImage(svgUrl);
const scale = 3;
const canvas = document.createElement("canvas");
canvas.width = width * scale;
canvas.height = height * scale;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Canvas rendering is unavailable.");
}
context.scale(scale, scale);
context.drawImage(image, 0, 0, width, height);
const pngBlob = await canvasToBlob(canvas);
const pngUrl = URL.createObjectURL(pngBlob);
try {
const link = document.createElement("a");
link.href = pngUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
} finally {
URL.revokeObjectURL(pngUrl);
}
} finally {
URL.revokeObjectURL(svgUrl);
}
}
function inlineComputedStyles(source: Element, target: Element): void {
if (target instanceof HTMLElement || target instanceof SVGElement) {
const computed = window.getComputedStyle(source);
for (const property of Array.from(computed)) {
target.style.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
}
}
const sourceChildren = Array.from(source.children);
const targetChildren = Array.from(target.children);
sourceChildren.forEach((child, index) => {
const targetChild = targetChildren[index];
if (targetChild) {
inlineComputedStyles(child, targetChild);
}
});
}
function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error("Failed to render export image."));
image.src = url;
});
}
function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error("Failed to encode PNG image."));
}
}, "image/png");
});
}

View file

@ -674,7 +674,7 @@ export function normalizeOverviewWidget(value: unknown): OverviewWidgetConfig |
}
export function normalizeOverviewWidgetType(value: unknown): OverviewWidgetType | undefined {
return typeof value === "string" && ["account-balance", "client-analysis", "metric", "model-distribution", "provider-analysis", "system-status", "token-activity", "token-mix", "usage-trend"].includes(value)
return typeof value === "string" && ["account-balance", "client-analysis", "metric", "model-distribution", "provider-analysis", "share-fuel-cockpit", "share-model-leaderboard", "share-route-map", "share-spend-receipt", "share-token-calendar", "share-usage-wrapped", "system-status", "token-activity", "token-mix", "usage-trend"].includes(value)
? value as OverviewWidgetType
: undefined;
}
@ -754,6 +754,11 @@ export function overviewWidgetVariantOptions(type: OverviewWidgetType): Array<{
{ label: "Compact", value: "compact" }
];
}
if (isShareOverviewWidgetType(type)) {
return [
{ label: "Card", value: "card" }
];
}
return [
{ label: "Table", value: "table" },
{ label: "Compact", value: "compact" }
@ -775,6 +780,7 @@ export function defaultOverviewWidgetSize(type: OverviewWidgetType): OverviewWid
if (type === "client-analysis" || type === "provider-analysis") return "2:2";
if (type === "usage-trend") return "3:2";
if (type === "system-status") return "4:1";
if (isShareOverviewWidgetType(type)) return "1:4";
return "4:2";
}
@ -786,6 +792,7 @@ export function defaultOverviewWidgetVariant(type: OverviewWidgetType): Overview
if (type === "token-activity") return "heatmap";
if (type === "usage-trend") return "composed";
if (type === "system-status") return "timeline";
if (isShareOverviewWidgetType(type)) return "card";
return "table";
}
@ -795,12 +802,24 @@ export function constrainOverviewWidgetSize(
variant: OverviewWidgetVariant,
accountProvider?: string
): OverviewWidgetSize {
if (isShareOverviewWidgetType(type)) {
return overviewWidgetSizeAtLeast(size, 1, 4);
}
if (type !== "account-balance" || variant !== "compact" || accountProvider?.trim()) {
return size;
}
return overviewWidgetSizeAtLeast(size, 2, 2);
}
function isShareOverviewWidgetType(type: OverviewWidgetType): boolean {
return type === "share-fuel-cockpit" ||
type === "share-model-leaderboard" ||
type === "share-route-map" ||
type === "share-spend-receipt" ||
type === "share-token-calendar" ||
type === "share-usage-wrapped";
}
function overviewWidgetSizeAtLeast(size: OverviewWidgetSize, minWidth: 1 | 2 | 3 | 4, minHeight: 1 | 2 | 3 | 4): OverviewWidgetSize {
const [widthText, heightText] = size.split(":");
const width = overviewWidgetDimensionAtLeast(widthText, minWidth);

View file

@ -279,6 +279,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Bar": "Bar",
"Bars": "Bars",
"Breakdown component": "Breakdown component",
"Card": "Card",
"Cards": "Cards",
"Change widget type": "Change widget type",
"Client Analysis": "Client Analysis",
@ -340,6 +341,27 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Widget": "Widget",
"Widget size": "Widget size",
"Wide": "Wide",
"Share cards": "Share cards",
"Share card": "Share card",
"Save social-ready cards as PNG images": "Save social-ready cards as PNG images",
"Social media PNG cards": "Social media PNG cards",
"PNG export": "PNG export",
"Save image": "Save image",
"Saving": "Saving",
"Export failed": "Export failed",
"AI Usage Wrapped": "AI Usage Wrapped",
"CCR Route Map": "CCR Route Map",
"Model Leaderboard": "Model Leaderboard",
"AI Fuel Cockpit": "AI Fuel Cockpit",
"Token Calendar Poster": "Token Calendar Poster",
"Spend Receipt": "Spend Receipt",
"tokens routed through CCR": "tokens routed through CCR",
"Top model": "Top model",
"Top provider": "Top provider",
"No provider activity": "No provider activity",
"Peak day": "Peak day",
"Generated by CCR": "Generated by CCR",
"Range": "Range",
"Virtual model": "Fusion",
"Virtual Models": "Fusion",
"Add Virtual Model": "Add Fusion",
@ -967,6 +989,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Bar": "柱状图",
"Bars": "横条",
"Breakdown component": "构成组件",
"Card": "卡片",
"Cards": "卡片",
"Change widget type": "切换组件类型",
"Client or provider": "客户端或供应商",
@ -1034,6 +1057,27 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Widget": "组件",
"Widget size": "组件大小",
"Wide": "宽",
"Share cards": "分享卡片",
"Share card": "分享卡片",
"Save social-ready cards as PNG images": "将适合社交媒体发布的卡片保存为 PNG 图片",
"Social media PNG cards": "适合社交媒体发布的 PNG 卡片",
"PNG export": "PNG 导出",
"Save image": "保存图片",
"Saving": "正在保存",
"Export failed": "导出失败",
"AI Usage Wrapped": "AI 用量年报",
"CCR Route Map": "CCR 路由图",
"Model Leaderboard": "模型排行榜",
"AI Fuel Cockpit": "AI 燃料仪表",
"Token Calendar Poster": "Token 日历海报",
"Spend Receipt": "消费小票",
"tokens routed through CCR": "通过 CCR 路由的令牌",
"Top model": "最高频模型",
"Top provider": "最高频供应商",
"No provider activity": "暂无供应商活动",
"Peak day": "峰值日期",
"Generated by CCR": "由 CCR 生成",
"Range": "范围",
"W": "三",
"Yes": "是",
"Set as default provider": "设为默认供应商",
@ -1123,6 +1167,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Base model mode": "基础模型模式",
"Client tools": "客户端工具",
"Client-visible": "客户端可见",
"Clients": "客户端",
"Command": "命令",
"Command is required.": "命令不能为空。",
"Decorate only": "仅装饰",

View file

@ -6,8 +6,12 @@ import type {
AgentAnalysisTracePayloadFullResult,
AgentAnalysisTracePayloadRequest,
AppConfig,
AppCaptureElementPngRequest,
AppCaptureElementPngResult,
AppDataExportResult,
AppInfo,
AppImageExportTargetRequest,
AppImageExportTargetResult,
AppSaveConfigOptions,
AppUpdateStatus,
ApiKeyConfig,
@ -73,6 +77,7 @@ declare global {
applyClaudeAppGateway: (config?: AppConfig) => Promise<ClaudeAppGatewayApplyResult>;
applyProfile: () => Promise<ProfileApplyResult>;
cancelBotGatewayQrLogin: (request: BotGatewayQrLoginCancelRequest) => Promise<BotGatewayQrLoginCancelResult>;
captureElementPng?: (request: AppCaptureElementPngRequest) => Promise<AppCaptureElementPngResult>;
checkProviderConnectivity: (request: GatewayProviderConnectivityCheckRequest) => Promise<GatewayProviderConnectivityCheckReport>;
closeBotGatewayQrWindow: (request: BotGatewayQrWindowCloseRequest) => Promise<BotGatewayQrWindowCloseResult>;
clearProxyNetworkCaptures: () => Promise<ProxyNetworkSnapshot>;
@ -108,6 +113,7 @@ declare global {
openBotGatewayQrWindow: (request: BotGatewayQrWindowOpenRequest) => Promise<BotGatewayQrWindowOpenResult>;
openExternal: (url: string) => Promise<void>;
openProfile: (request: ProfileOpenRequest) => Promise<ProfileOpenResult>;
prepareImageExportTarget?: (request: AppImageExportTargetRequest) => Promise<AppImageExportTargetResult>;
probeProviderCandidates: (request: GatewayProviderProbeCandidatesRequest) => Promise<GatewayProviderProbeCandidateResult | undefined>;
probeProvider: (request: GatewayProviderProbeRequest) => Promise<GatewayProviderProbeResult>;
quitApp: () => Promise<void>;

View file

@ -18,6 +18,37 @@ export type AppDataExportResult = {
file?: string;
};
export type AppCaptureElementPngRequest = {
borderRadius?: number;
exportId?: string;
fileName: string;
output?: {
height: number;
width: number;
};
rect: {
height: number;
width: number;
x: number;
y: number;
};
};
export type AppCaptureElementPngResult = {
canceled: boolean;
file?: string;
};
export type AppImageExportTargetRequest = {
fileName: string;
};
export type AppImageExportTargetResult = {
canceled: boolean;
exportId?: string;
file?: string;
};
export type AppUpdateState =
| "idle"
| "checking"
@ -851,6 +882,12 @@ export type OverviewWidgetType =
| "metric"
| "model-distribution"
| "provider-analysis"
| "share-fuel-cockpit"
| "share-model-leaderboard"
| "share-route-map"
| "share-spend-receipt"
| "share-token-calendar"
| "share-usage-wrapped"
| "system-status"
| "token-activity"
| "token-mix"

View file

@ -1,5 +1,6 @@
export const IPC_CHANNELS = {
appBeforeQuit: "ccr:app:before-quit",
appCaptureElementPng: "ccr:app:capture-element-png",
appCloseTray: "ccr:app:close-tray",
appDetectProviderIcon: "ccr:app:detect-provider-icon",
appExportData: "ccr:app:export-data",
@ -46,6 +47,7 @@ export const IPC_CHANNELS = {
appProbeProviderCandidates: "ccr:app:probe-provider-candidates",
appProviderDeepLink: "ccr:app:provider-deep-link",
appGetPluginMarketplace: "ccr:app:get-plugin-marketplace",
appPrepareImageExportTarget: "ccr:app:prepare-image-export-target",
appQuit: "ccr:app:quit",
appRevealProxyCertificate: "ccr:app:reveal-proxy-certificate",
appRestartProxy: "ccr:app:restart-proxy",

View file

@ -20,7 +20,13 @@ test("OverviewView renders every overview widget type", () => {
{ enabled: true, id: "token-mix", size: "2:2", type: "token-mix", variant: "stacked" },
{ enabled: true, id: "models", size: "2:2", type: "model-distribution", variant: "donut" },
{ enabled: true, id: "clients", size: "4:2", type: "client-analysis", variant: "table" },
{ enabled: true, id: "providers", size: "4:2", type: "provider-analysis", variant: "table" }
{ enabled: true, id: "providers", size: "4:2", type: "provider-analysis", variant: "table" },
{ enabled: true, id: "share-usage", size: "1:4", type: "share-usage-wrapped", variant: "card" },
{ enabled: true, id: "share-routes", size: "1:4", type: "share-route-map", variant: "card" },
{ enabled: true, id: "share-models", size: "1:4", type: "share-model-leaderboard", variant: "card" },
{ enabled: true, id: "share-fuel", size: "1:4", type: "share-fuel-cockpit", variant: "card" },
{ enabled: true, id: "share-calendar", size: "1:4", type: "share-token-calendar", variant: "card" },
{ enabled: true, id: "share-receipt", size: "1:4", type: "share-spend-receipt", variant: "card" }
];
const html = renderToStaticMarkup(
@ -51,6 +57,13 @@ test("OverviewView renders every overview widget type", () => {
assert.match(html, /Provider Analysis/);
assert.match(html, /claude-code/);
assert.match(html, /openai/);
assert.match(html, /Save image/);
assert.match(html, /AI Usage Wrapped/);
assert.match(html, /CCR Route Map/);
assert.match(html, /Model Leaderboard/);
assert.match(html, /AI Fuel Cockpit/);
assert.match(html, /Token Calendar Poster/);
assert.match(html, /Spend Receipt/);
});
test("OverviewView renders the empty widget layout state", () => {