Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/components/usage/UsageDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,18 @@ export function UsageDashboard() {
return getUsageRangePresetLabel(range.preset, t);
}

return `${new Date(resolvedRange.startDate * 1000).toLocaleString(locale)} - ${new Date(
resolvedRange.endDate * 1000,
).toLocaleString(locale)}`;
const startStr = new Date(resolvedRange.startDate * 1000).toLocaleString(
locale,
);

if (range.liveEndTime) {
return `${startStr} → ${t("usage.liveEndTimeNow", "现在")}`;
}

const endStr = new Date(resolvedRange.endDate * 1000).toLocaleString(
locale,
);
return `${startStr} - ${endStr}`;
}, [locale, range, resolvedRange.endDate, resolvedRange.startDate, t]);

// 顶栏下拉的选项池:Provider 列表只跟应用/时间范围走(不受自身选中值影响),
Expand Down
79 changes: 70 additions & 9 deletions src/components/usage/UsageDateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ChevronRight,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import {
Popover,
Expand Down Expand Up @@ -117,6 +118,9 @@ export function UsageDateRangePicker({
);
const [draftStart, setDraftStart] = useState(resolvedRange.startDate);
const [draftEnd, setDraftEnd] = useState(resolvedRange.endDate);
const [draftLiveEnd, setDraftLiveEnd] = useState(
selection.preset === "custom" ? (selection.liveEndTime ?? false) : false,
);
const [displayMonth, setDisplayMonth] = useState(
() =>
new Date(
Expand All @@ -136,6 +140,9 @@ export function UsageDateRangePicker({
const r = resolveUsageRange(selection);
setDraftStart(r.startDate);
setDraftEnd(r.endDate);
setDraftLiveEnd(
selection.preset === "custom" ? (selection.liveEndTime ?? false) : false,
);
setDisplayMonth(
new Date(
fromTs(r.startDate).getFullYear(),
Expand All @@ -147,6 +154,15 @@ export function UsageDateRangePicker({
setError(null);
}, [open, selection]);

// Keep draftEnd ticking when live mode is active and popover is open
useEffect(() => {
if (!open || !draftLiveEnd) return;
const tick = () => setDraftEnd(Math.floor(Date.now() / 1000));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [open, draftLiveEnd]);

const calendarDays = useMemo(
() => getCalendarDays(displayMonth),
[displayMonth],
Expand All @@ -169,6 +185,14 @@ export function UsageDateRangePicker({
/* Pick a date from the calendar */
const handleDatePick = (day: Date) => {
setError(null);

// When live end time is active, calendar only controls start date
if (draftLiveEnd) {
const nextTs = setDateKeepTime(draftStart, day);
setDraftStart(nextTs);
return;
}

const nextTs = setDateKeepTime(
activeField === "start" ? draftStart : draftEnd,
day,
Expand Down Expand Up @@ -211,6 +235,7 @@ export function UsageDateRangePicker({
preset: "custom",
customStartDate: draftStart,
customEndDate: draftEnd,
liveEndTime: draftLiveEnd,
});
setOpen(false);
};
Expand All @@ -222,6 +247,7 @@ export function UsageDateRangePicker({
/* ── Field card (start / end) ── */
const renderField = (field: DraftField) => {
const isActive = activeField === field;
const isEndLive = field === "end" && draftLiveEnd;
const ts = field === "start" ? draftStart : draftEnd;
const setTs = field === "start" ? setDraftStart : setDraftEnd;
const label =
Expand All @@ -232,40 +258,58 @@ export function UsageDateRangePicker({
return (
<div
className={cn(
"rounded-lg border px-3 py-2 cursor-pointer transition-all",
isActive
? "border-primary ring-1 ring-primary/30 bg-primary/5"
: "border-border/50 hover:border-border",
"rounded-lg border px-3 py-2 transition-all",
isEndLive
? "border-border/30 bg-muted/30 cursor-not-allowed opacity-50"
: isActive
? "border-primary ring-1 ring-primary/30 bg-primary/5 cursor-pointer"
: "border-border/50 hover:border-border cursor-pointer",
)}
onClick={() => setActiveField(field)}
onClick={() => {
if (!isEndLive) setActiveField(field);
}}
>
<div className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
{label}
</div>
<div className="flex items-center gap-1.5">
<Input
type="date"
className="h-7 flex-1 border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0"
className={cn(
"h-7 flex-1 border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0",
isEndLive && "pointer-events-none",
)}
value={fmtDate(ts)}
onChange={(e) => {
if (isEndLive) return;
const next = parseDateInput(ts, e.target.value);
setTs(next);
const d = fromTs(next);
setDisplayMonth(new Date(d.getFullYear(), d.getMonth(), 1));
setError(null);
}}
onFocus={() => setActiveField(field)}
onFocus={() => {
if (!isEndLive) setActiveField(field);
}}
readOnly={isEndLive}
/>
<Input
type="time"
step={60}
className="h-7 w-[90px] flex-none border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0"
className={cn(
"h-7 w-[90px] flex-none border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0",
isEndLive && "pointer-events-none",
)}
value={fmtTime(ts)}
onChange={(e) => {
if (isEndLive) return;
setTs(parseTimeInput(ts, e.target.value));
setError(null);
}}
onFocus={() => setActiveField(field)}
onFocus={() => {
if (!isEndLive) setActiveField(field);
}}
readOnly={isEndLive}
/>
</div>
</div>
Expand Down Expand Up @@ -318,6 +362,23 @@ export function UsageDateRangePicker({
{renderField("start")}
{renderField("end")}

<label className="flex items-center gap-2 cursor-pointer select-none">
<Checkbox
checked={draftLiveEnd}
onCheckedChange={(checked) => {
const live = checked === true;
setDraftLiveEnd(live);
if (live) {
setDraftEnd(Math.floor(Date.now() / 1000));
setActiveField("start");
}
}}
/>
<span className="text-xs text-muted-foreground">
{t("usage.liveEndTime", "结束时间跟随当前时刻")}
</span>
</label>

{error && <p className="text-xs text-destructive">{error}</p>}

<div className="flex gap-2 pt-1">
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,8 @@
"customRangeHint": "Supports both date and time",
"startTime": "Start Time",
"endTime": "End Time",
"liveEndTime": "End time follows current time",
"liveEndTimeNow": "Now",
"input": "Input",
"output": "Output",
"cacheWrite": "Creation",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,8 @@
"customRangeHint": "日付と時刻の両方に対応",
"startTime": "開始時刻",
"endTime": "終了時刻",
"liveEndTime": "終了時刻を現在時刻に追従",
"liveEndTimeNow": "現在",
"input": "Input",
"output": "Output",
"cacheWrite": "作成",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,8 @@
"customRangeHint": "支援日期與時間",
"startTime": "開始時間",
"endTime": "結束時間",
"liveEndTime": "結束時間跟隨當前時刻",
"liveEndTimeNow": "現在",
"input": "Input",
"output": "Output",
"cacheWrite": "建立",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,8 @@
"customRangeHint": "支持日期与时间",
"startTime": "开始时间",
"endTime": "结束时间",
"liveEndTime": "结束时间跟随当前时刻",
"liveEndTimeNow": "现在",
"input": "Input",
"output": "Output",
"cacheWrite": "创建",
Expand Down
20 changes: 19 additions & 1 deletion src/lib/query/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type RequestLogsKey = {
preset: UsageRangeSelection["preset"];
customStartDate?: number;
customEndDate?: number;
liveEndTime?: boolean;
appType?: string;
providerName?: string;
model?: string;
Expand All @@ -40,13 +41,15 @@ export const usageKeys = {
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"summary",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
Expand All @@ -55,14 +58,16 @@ export const usageKeys = {
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
filters?: Pick<UsageScopeFilters, "providerName" | "model">,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"summary-by-app",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.providerName ?? null,
filters?.model ?? null,
] as const,
Expand All @@ -71,13 +76,15 @@ export const usageKeys = {
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"trends",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
Expand All @@ -87,13 +94,15 @@ export const usageKeys = {
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"provider-stats",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
Expand All @@ -103,13 +112,15 @@ export const usageKeys = {
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"model-stats",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
Expand All @@ -121,6 +132,7 @@ export const usageKeys = {
key.preset,
key.customStartDate ?? 0,
key.customEndDate ?? 0,
key.liveEndTime ?? false,
key.appType ?? "",
key.providerName ?? "",
key.model ?? "",
Expand Down Expand Up @@ -159,6 +171,7 @@ export function useUsageSummary(
range.customStartDate,
range.customEndDate,
effective,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
Expand Down Expand Up @@ -186,6 +199,7 @@ export function useUsageSummaryByApp(
range.customStartDate,
range.customEndDate,
filters,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
Expand Down Expand Up @@ -213,6 +227,7 @@ export function useUsageTrends(
range.customStartDate,
range.customEndDate,
effective,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
Expand Down Expand Up @@ -241,6 +256,7 @@ export function useProviderStats(
range.customStartDate,
range.customEndDate,
effective,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
Expand Down Expand Up @@ -269,6 +285,7 @@ export function useModelStats(
range.customStartDate,
range.customEndDate,
effective,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
Expand Down Expand Up @@ -296,6 +313,7 @@ export function useRequestLogs({
preset: range.preset,
customStartDate: range.customStartDate,
customEndDate: range.customEndDate,
liveEndTime: range.liveEndTime,
appType: filters.appType,
providerName: filters.providerName,
model: filters.model,
Expand Down
4 changes: 3 additions & 1 deletion src/lib/usageRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export function resolveUsageRange(
};
case "custom": {
const startDate = selection.customStartDate ?? endDate - DAY_SECONDS;
const customEndDate = selection.customEndDate ?? endDate;
const customEndDate = selection.liveEndTime
? endDate
: (selection.customEndDate ?? endDate);
Comment on lines +52 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include liveEndTime in usage query keys

When liveEndTime is true this branch changes the actual request end date to Date.now(), but the React Query keys in src/lib/query/usage.ts still distinguish ranges only by preset, customStartDate, and customEndDate. A live custom range and a fixed custom range with the same stored endpoints can therefore share and overwrite the same cache entry; after the live range refreshes, switching to the fixed range can show data fetched through "now" until another refetch happens (or indefinitely when auto-refresh is off).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

return {
startDate,
endDate: customEndDate,
Expand Down
3 changes: 3 additions & 0 deletions src/types/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ export interface UsageRangeSelection {
preset: UsageRangePreset;
customStartDate?: number;
customEndDate?: number;
/** When true (custom mode only), endDate resolves to "now" instead of the
* fixed customEndDate snapshot, and the end-time field becomes read-only. */
liveEndTime?: boolean;
}

/**
Expand Down
Loading