perf(dashboard): 仪表盘加载提速
- models-dev 快照移出主 bundle(1.35MB→515KB),改为 /models-dev.json 运行时异步加载,就绪后重渲染 - Rust 启动时后台预热价格索引(warm_price_index),首次 get_summary 不再卡 - get_summary 扫描结果 8s TTL 缓存(按 home),切 tab/改范围复用;刷新按钮 refresh=true 强制重扫
This commit is contained in:
1 parent
9fd22ef55a
commit
1fecb161a9
7 files changed
+64112
-23
No files matched your search
+10
-2
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
@@ -12,7 +12,7 @@ import { UsageConfigModal } from "./components/UsageConfigModal";
|
||||
import { PresetPickerModal } from "./components/PresetPickerModal";
|
||||
import { useTranslation } from "./i18n";
|
||||
import { getDefaultMaxContextSize } from "./lib/model-defaults";
|
||||
import { getModelRef } from "./lib/models-dev";
|
||||
import { getModelRef, modelsDevReady, onModelsDevReady } from "./lib/models-dev";
|
||||
import {
|
||||
presetToProviderAndModels,
|
||||
type ProviderPreset,
|
||||
@@ -121,6 +121,14 @@ export default function App() {
|
||||
}).catch(() => {});
|
||||
}, [loading, config, agent]);
|
||||
|
||||
// Load the models.dev index in the background (not in the main bundle)
|
||||
// and re-render once it lands so context/capability columns fill in.
|
||||
const [, forceModelsDevReady] = useReducer((x: number) => x + 1, 0);
|
||||
useEffect(() => {
|
||||
onModelsDevReady(forceModelsDevReady);
|
||||
void modelsDevReady();
|
||||
}, []);
|
||||
|
||||
const providers = useMemo(
|
||||
() => (config ? Object.values(config.providers) : []),
|
||||
[config]
|
||||
|
||||
@@ -71,7 +71,7 @@ export function DashboardPage() {
|
||||
<div className="text-red-400 mb-2">{t("dashLoadFailed")}</div>
|
||||
<div className="text-sm text-content-muted">{error}</div>
|
||||
<button
|
||||
onClick={() => refresh()}
|
||||
onClick={() => refresh(true)}
|
||||
className="mt-4 px-4 py-2 rounded bg-blue-600 hover:bg-blue-700 text-white text-sm"
|
||||
>
|
||||
{t("retry")}
|
||||
@@ -145,7 +145,7 @@ export function DashboardPage() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refresh()}
|
||||
onClick={() => refresh(true)}
|
||||
disabled={loading}
|
||||
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2 text-content-muted hover:text-content-primary disabled:opacity-50"
|
||||
>
|
||||
|
||||
@@ -23,11 +23,14 @@ export function useDashboard() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const refresh = useCallback(async (force = false) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await invoke<SummaryResult>("get_summary", { range });
|
||||
const result = await invoke<SummaryResult>("get_summary", {
|
||||
range,
|
||||
refresh: force,
|
||||
});
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
|
||||
+57
-15
@@ -1,9 +1,15 @@
|
||||
import snapshot from "./models-dev.json";
|
||||
|
||||
/**
|
||||
* Compact per-model reference data from models.dev
|
||||
* (see scripts/fetch-models-dev.mjs).
|
||||
*
|
||||
* The snapshot (~1.3 MB JSON) is NOT bundled into the main chunk anymore:
|
||||
* parsing a 1.3 MB JSON literal at startup blocks first paint. It is served
|
||||
* as a static asset (`/models-dev.json`) and loaded once in the background.
|
||||
* `getModelRef` stays synchronous and returns undefined until the index is
|
||||
* ready — callers already fall back to defaults — and `modelsDevReady()`
|
||||
* lets the app re-render once the index arrives.
|
||||
*/
|
||||
|
||||
export interface ModelCost {
|
||||
input?: number;
|
||||
output?: number;
|
||||
@@ -23,22 +29,58 @@ export interface ModelRef {
|
||||
cost?: ModelCost;
|
||||
}
|
||||
|
||||
const raw = snapshot as Record<string, unknown>;
|
||||
let byLowerKey: Record<string, ModelRef> | null = null;
|
||||
let readyListeners: Array<() => void> = [];
|
||||
|
||||
// Drop the "last_updated" metadata key; the rest is "<provider>/<model>".
|
||||
const data: Record<string, ModelRef> = {};
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
if (key !== "last_updated" && value && typeof value === "object") {
|
||||
data[key] = value as ModelRef;
|
||||
function buildIndex(raw: Record<string, unknown>) {
|
||||
const data: Record<string, ModelRef> = {};
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
if (key !== "last_updated" && value && typeof value === "object") {
|
||||
data[key] = value as ModelRef;
|
||||
}
|
||||
}
|
||||
// models.dev keys are "<lab>/<model>" and may be mixed-case (e.g.
|
||||
// "minimax/MiniMax-M3"), while provider APIs return bare model ids
|
||||
// (e.g. "kimi-k2.5"). Index lowercase for tolerant matching.
|
||||
const lower: Record<string, ModelRef> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
lower[key.toLowerCase()] = value;
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
|
||||
// models.dev keys are "<lab>/<model>" and may be mixed-case (e.g.
|
||||
// "minimax/MiniMax-M3"), while provider APIs return bare model ids
|
||||
// (e.g. "kimi-k2.5"). Index lowercase for tolerant matching.
|
||||
const byLowerKey: Record<string, ModelRef> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
byLowerKey[key.toLowerCase()] = value;
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
|
||||
export function modelsDevReady(): Promise<void> {
|
||||
if (!loadPromise) {
|
||||
loadPromise = fetch(`${import.meta.env.BASE_URL}models-dev.json`)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`models-dev.json HTTP ${r.status}`);
|
||||
return r.json() as Promise<Record<string, unknown>>;
|
||||
})
|
||||
.then((raw) => {
|
||||
byLowerKey = buildIndex(raw);
|
||||
const listeners = readyListeners;
|
||||
readyListeners = [];
|
||||
for (const cb of listeners) cb();
|
||||
})
|
||||
.catch((err) => {
|
||||
// A failed load is permanent for this session: fall back to defaults.
|
||||
byLowerKey = {};
|
||||
loadPromise = null; // allow one retry next time
|
||||
console.warn("models-dev.json load failed:", err);
|
||||
});
|
||||
}
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
/** Register a callback invoked once the models.dev index is ready. */
|
||||
export function onModelsDevReady(cb: () => void): void {
|
||||
if (byLowerKey) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
readyListeners.push(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +91,7 @@ for (const [key, value] of Object.entries(data)) {
|
||||
*/
|
||||
export function getModelRef(modelId: string): ModelRef | undefined {
|
||||
const id = modelId.trim().toLowerCase();
|
||||
if (!id) return undefined;
|
||||
if (!id || !byLowerKey) return undefined;
|
||||
const exact = byLowerKey[id];
|
||||
if (exact) return exact;
|
||||
const suffix = `/${id}`;
|
||||
|
||||
Reference in new issue
Block a user