feat(subagent): v0.7.5 — 适配 kimi-code 0.36.0 子代理模型池(双写兼容/池管理/force/前置校验)+ 热力图详情按需加载 + models.dev 刷新(GLM-5.3)
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s
This commit is contained in:
1 parent
89c71fbfe6
commit
9a3875d990
19 files changed
+152041
-140593
No files matched your search
@@ -8,12 +8,19 @@ import {
|
||||
clearSecondaryModel,
|
||||
forcedEnvValue,
|
||||
getExperimentalFlags,
|
||||
getSecondaryModel,
|
||||
getSubagentModelPool,
|
||||
isFlagLockedByEnv,
|
||||
isMasterEnvOn,
|
||||
removeSubagentPoolEntry,
|
||||
setExperimentalFlag,
|
||||
setSecondaryModelOnly,
|
||||
setSubagentDefault,
|
||||
setSubagentForce,
|
||||
upsertSubagentPoolEntry,
|
||||
validateSubagentPool,
|
||||
type ExperimentalFlagDef,
|
||||
type PoolValidationError,
|
||||
type ValidationErrorKey,
|
||||
} from "../lib/subagent-settings";
|
||||
import { Card, Toggle } from "./ui/controls";
|
||||
|
||||
@@ -44,6 +51,97 @@ const FLAG_LABELS: Record<string, { name: TranslationKey; desc: TranslationKey }
|
||||
},
|
||||
};
|
||||
|
||||
/** Validation-error i18n key per engine rule, for the pre-write self-check. */
|
||||
const POOL_ERROR_KEYS: Record<ValidationErrorKey, TranslationKey> = {
|
||||
primaryReserved: "secondaryPoolErrorPrimaryReserved",
|
||||
defaultRequired: "secondaryPoolErrorDefaultRequired",
|
||||
defaultNotInPool: "secondaryPoolErrorDefaultNotInPool",
|
||||
aliasNotInModels: "secondaryPoolErrorAliasNotInModels",
|
||||
forceRequiresDefault: "secondaryPoolErrorForceRequiresDefault",
|
||||
forceExcludesModels: "secondaryPoolErrorForceExcludesModels",
|
||||
};
|
||||
|
||||
/**
|
||||
* One row of the subagent model pool: default radio + alias + editable
|
||||
* description (saved on blur/Enter via the parent's write chain) + remove.
|
||||
* The description input keeps a local draft so typing is not persisted per
|
||||
* keystroke; everything else is derived from `rawOther` on each render.
|
||||
*/
|
||||
function PoolEntryRow({
|
||||
alias,
|
||||
displayName,
|
||||
description,
|
||||
isDefault,
|
||||
canRemove,
|
||||
onSetDefault,
|
||||
onSave,
|
||||
onRemove,
|
||||
}: {
|
||||
alias: string;
|
||||
displayName: string | null;
|
||||
description: string;
|
||||
isDefault: boolean;
|
||||
canRemove: boolean;
|
||||
onSetDefault: (alias: string) => void;
|
||||
onSave: (alias: string, description: string) => void;
|
||||
onRemove: (alias: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [draft, setDraft] = useState(description);
|
||||
|
||||
const commit = () => {
|
||||
if (draft === description) return;
|
||||
onSave(alias, draft);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
checked={isDefault}
|
||||
aria-label={t("setDefault")}
|
||||
onChange={() => onSetDefault(alias)}
|
||||
className="shrink-0 accent-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="shrink-0 font-medium text-content-primary">{alias}</span>
|
||||
{displayName && (
|
||||
<span className="shrink-0 text-xs text-content-muted">{displayName}</span>
|
||||
)}
|
||||
{isDefault && (
|
||||
<span className="shrink-0 text-xs text-blue-600 dark:text-blue-400 border border-blue-500/30 rounded px-1">
|
||||
{t("default")}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={draft}
|
||||
placeholder={t("secondaryPoolDescPlaceholder")}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
} else if (e.key === "Escape") {
|
||||
setDraft(description);
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
className="flex-1 min-w-0 bg-input border border-border rounded px-2 py-1 text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
{canRemove && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemove(alias)}
|
||||
className="shrink-0 px-2 py-1 text-xs border border-border rounded hover:bg-hover-2 text-content-muted focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
{t("secondaryPoolRemove")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubagentSettingsPage({
|
||||
rawOther,
|
||||
models,
|
||||
@@ -52,6 +150,10 @@ export function SubagentSettingsPage({
|
||||
}: SubagentSettingsPageProps) {
|
||||
const { t } = useTranslation();
|
||||
const [env, setEnv] = useState<ExperimentalEnvStatus | null>(null);
|
||||
/** Pre-write validation failures (fallback guard; normal flows stay clear). */
|
||||
const [poolErrors, setPoolErrors] = useState<PoolValidationError[]>([]);
|
||||
/** Local draft for the "add pool entry" select (reset after each pick). */
|
||||
const [addSelection, setAddSelection] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
invoke<ExperimentalEnvStatus>("get_experimental_env_status")
|
||||
@@ -60,7 +162,17 @@ export function SubagentSettingsPage({
|
||||
}, []);
|
||||
|
||||
const flags = getExperimentalFlags(rawOther);
|
||||
const secondary = getSecondaryModel(rawOther);
|
||||
const pool = getSubagentModelPool(rawOther);
|
||||
const poolKeys = pool ? Object.keys(pool.models) : [];
|
||||
// View selection: no/empty section or a bare legacy `model` → simple view;
|
||||
// a `models` table or `default_model` → pool management view.
|
||||
const isPoolView =
|
||||
!!pool && (poolKeys.length > 0 || pool.defaultModel !== undefined);
|
||||
// Effective default: v2 pool key first, legacy v1 recipe key as fallback.
|
||||
const effectiveDefault = pool ? pool.defaultModel ?? pool.model : undefined;
|
||||
// "Upgrade" only makes sense for the bare legacy form and when `force` is
|
||||
// off — upgrading with force would create an invalid force+pool-table combo.
|
||||
const canUpgrade = !!pool && !isPoolView && pool.model !== undefined && !pool.force;
|
||||
const masterOn = isMasterEnvOn(env);
|
||||
const secondaryFlag = EXPERIMENTAL_FLAGS[0]; // "secondary-model"
|
||||
const otherFlags = EXPERIMENTAL_FLAGS.slice(1);
|
||||
@@ -75,10 +187,25 @@ export function SubagentSettingsPage({
|
||||
|
||||
const aliasList = Object.keys(models).sort();
|
||||
|
||||
const aliasLabel = (alias: string): string =>
|
||||
models[alias]?.display_name
|
||||
? `${alias}(${models[alias].display_name})`
|
||||
: alias;
|
||||
|
||||
// Rows of the pool view: every `models` table key plus the effective default
|
||||
// when it is not already in the table (the implicit single-entry form).
|
||||
const implicitDefault =
|
||||
pool && effectiveDefault && !(effectiveDefault in pool.models)
|
||||
? [effectiveDefault]
|
||||
: [];
|
||||
const poolEntries = [...poolKeys, ...implicitDefault];
|
||||
// Aliases that can still be added to the pool (not already in the table).
|
||||
const addableAliases = aliasList.filter((a) => !(a in (pool?.models ?? {})));
|
||||
|
||||
// Inherited settings shown for the picked model: subagents bind the pointed
|
||||
// entry directly (no patch fields), so context size and effort come from the
|
||||
// model entry / global [thinking], not from a `[secondary_model]` patch.
|
||||
const selectedEntry = secondary.model ? models[secondary.model] : undefined;
|
||||
const selectedEntry = effectiveDefault ? models[effectiveDefault] : undefined;
|
||||
const inheritedContext = selectedEntry?.max_context_size;
|
||||
const modelEffort =
|
||||
selectedEntry &&
|
||||
@@ -106,6 +233,61 @@ export function SubagentSettingsPage({
|
||||
return key ? t(key) : e;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fallback guard before any pool-related write: validate the *target* state
|
||||
* against the engine rules (mirrored by `validateSubagentPool`) and block the
|
||||
* write when it would produce an invalid config. Normal UI flows never trip
|
||||
* this — it protects against stale/divergent configs.
|
||||
*/
|
||||
const validateWrite = (nextRaw: unknown): boolean => {
|
||||
const nextPool = getSubagentModelPool(nextRaw);
|
||||
const errors = nextPool ? validateSubagentPool(nextPool, aliasList) : [];
|
||||
setPoolErrors(errors);
|
||||
return errors.length === 0;
|
||||
};
|
||||
|
||||
const handleUpgrade = () => {
|
||||
if (!effectiveDefault) return;
|
||||
const nextRaw = setSubagentDefault(rawOther, effectiveDefault);
|
||||
if (validateWrite(nextRaw)) onChange(nextRaw);
|
||||
};
|
||||
|
||||
const handleSetDefault = (alias: string) => {
|
||||
if (alias === effectiveDefault) return;
|
||||
const nextRaw = setSubagentDefault(rawOther, alias);
|
||||
if (validateWrite(nextRaw)) onChange(nextRaw);
|
||||
};
|
||||
|
||||
const handleSaveDescription = (alias: string, description: string) => {
|
||||
const nextRaw = upsertSubagentPoolEntry(rawOther, alias, description);
|
||||
if (validateWrite(nextRaw)) onChange(nextRaw);
|
||||
};
|
||||
|
||||
const handleRemoveEntry = (alias: string) => {
|
||||
const nextRaw = removeSubagentPoolEntry(rawOther, alias);
|
||||
if (validateWrite(nextRaw)) onChange(nextRaw);
|
||||
};
|
||||
|
||||
const handleAddEntry = (alias: string) => {
|
||||
const nextRaw = upsertSubagentPoolEntry(rawOther, alias, "");
|
||||
if (validateWrite(nextRaw)) onChange(nextRaw);
|
||||
};
|
||||
|
||||
const handleForceChange = (next: boolean) => {
|
||||
if (!pool) return;
|
||||
// Turning force on drops the `models` table — confirm when it is non-empty.
|
||||
if (next && Object.keys(pool.models).length > 0) {
|
||||
if (!confirm(t("secondaryPoolForceConfirm"))) return;
|
||||
}
|
||||
const nextRaw = setSubagentForce(rawOther, next);
|
||||
if (validateWrite(nextRaw)) onChange(nextRaw);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setPoolErrors([]);
|
||||
onChange(clearSecondaryModel(rawOther));
|
||||
};
|
||||
|
||||
const renderFlagRow = (flag: ExperimentalFlagDef) => {
|
||||
const labels = FLAG_LABELS[flag.id];
|
||||
const locked = masterOn || isFlagLockedByEnv(env, flag);
|
||||
@@ -194,36 +376,154 @@ export function SubagentSettingsPage({
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Subagent: secondary model picker + inherited settings */}
|
||||
{/* Subagent: secondary model picker / pool + inherited settings */}
|
||||
<Card title={t("secondaryModelSection")}>
|
||||
<label className="flex items-center gap-3 text-sm text-content-primary">
|
||||
<span className="text-content-muted">{t("secondaryModelLabel")}</span>
|
||||
<select
|
||||
className="flex-1 max-w-md bg-input border border-border rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
value={secondary.model ?? ""}
|
||||
onChange={(e) => {
|
||||
const alias = e.target.value;
|
||||
if (alias === "") {
|
||||
onChange(clearSecondaryModel(rawOther));
|
||||
} else {
|
||||
// Only the model reference is persisted — no patch fields.
|
||||
// Subagents bind the pointed entry directly and inherit its
|
||||
// settings; usage records stay keyed on the real alias
|
||||
// instead of Kimi Code's synthesized `__secondary__` entry.
|
||||
onChange(setSecondaryModelOnly(rawOther, alias));
|
||||
}
|
||||
}}
|
||||
{isPoolView && pool ? (
|
||||
<>
|
||||
{pool.force ? (
|
||||
/* Force on: the `models` table was dropped — only the pinned
|
||||
default is shown, entries and "add" are hidden. */
|
||||
<div className="rounded-lg border border-border bg-input/40 px-3 py-2.5">
|
||||
<div className="text-sm font-medium text-content-primary">
|
||||
🔒{" "}
|
||||
{t("secondaryPoolForceOn", {
|
||||
alias: aliasLabel(effectiveDefault ?? ""),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
{poolEntries.map((alias) => (
|
||||
<PoolEntryRow
|
||||
key={alias}
|
||||
alias={alias}
|
||||
displayName={models[alias]?.display_name ?? null}
|
||||
description={pool.models[alias] ?? ""}
|
||||
isDefault={alias === effectiveDefault}
|
||||
canRemove={alias in pool.models}
|
||||
onSetDefault={handleSetDefault}
|
||||
onSave={handleSaveDescription}
|
||||
onRemove={handleRemoveEntry}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-content-muted">
|
||||
{t("secondaryPoolDescHint")}
|
||||
</p>
|
||||
<label className="flex items-center gap-3 text-sm text-content-primary">
|
||||
<span className="shrink-0 text-content-muted">
|
||||
{t("secondaryPoolAddLabel")}
|
||||
</span>
|
||||
<select
|
||||
className="flex-1 max-w-md bg-input border border-border rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
value={addSelection}
|
||||
onChange={(e) => {
|
||||
const alias = e.target.value;
|
||||
setAddSelection("");
|
||||
if (alias) handleAddEntry(alias);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("secondaryPoolAddPlaceholder")}</option>
|
||||
{addableAliases.map((alias) => (
|
||||
<option key={alias} value={alias}>
|
||||
{aliasLabel(alias)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-content-primary">
|
||||
{t("secondaryPoolForce")}
|
||||
</div>
|
||||
<div className="text-xs text-content-muted">
|
||||
{t("secondaryPoolForceDesc")}
|
||||
</div>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={pool.force}
|
||||
ariaLabel={t("secondaryPoolForce")}
|
||||
onChange={handleForceChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
{t("secondaryModelClear")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<label className="flex items-center gap-3 text-sm text-content-primary">
|
||||
<span className="shrink-0 text-content-muted">
|
||||
{t("secondaryModelLabel")}
|
||||
</span>
|
||||
<select
|
||||
className="flex-1 max-w-md bg-input border border-border rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
value={effectiveDefault ?? ""}
|
||||
onChange={(e) => {
|
||||
const alias = e.target.value;
|
||||
if (alias === "") {
|
||||
setPoolErrors([]);
|
||||
onChange(clearSecondaryModel(rawOther));
|
||||
} else {
|
||||
// Only the model reference is persisted — no patch fields.
|
||||
// Subagents bind the pointed entry directly and inherit
|
||||
// its settings; usage records stay keyed on the real
|
||||
// alias instead of the synthesized `__secondary__` entry.
|
||||
const nextRaw = setSecondaryModelOnly(rawOther, alias);
|
||||
if (validateWrite(nextRaw)) onChange(nextRaw);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">{t("secondaryModelUnset")}</option>
|
||||
{aliasList.map((alias) => (
|
||||
<option key={alias} value={alias}>
|
||||
{aliasLabel(alias)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{pool?.force && (
|
||||
<span className="shrink-0 text-xs text-content-muted">
|
||||
🔒 {t("secondaryModelForceBadge")}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{canUpgrade && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUpgrade}
|
||||
className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
{t("secondaryPoolUpgrade")}
|
||||
</button>
|
||||
<span className="text-xs text-content-muted">
|
||||
{t("secondaryPoolUpgradeDesc")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{poolErrors.length > 0 && (
|
||||
<div
|
||||
role="alert"
|
||||
className="bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-500/30 rounded-lg px-3 py-2 text-sm text-red-700 dark:text-red-400 space-y-1"
|
||||
>
|
||||
<option value="">{t("secondaryModelUnset")}</option>
|
||||
{aliasList.map((alias) => (
|
||||
<option key={alias} value={alias}>
|
||||
{models[alias]?.display_name
|
||||
? `${alias}(${models[alias].display_name})`
|
||||
: alias}
|
||||
</option>
|
||||
{poolErrors.map((err, i) => (
|
||||
<div key={i}>{t(POOL_ERROR_KEYS[err.key], err.params)}</div>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-border" />
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -248,7 +548,7 @@ export function SubagentSettingsPage({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{secondary.model !== undefined && (
|
||||
{effectiveDefault !== undefined && (
|
||||
<div className="rounded-lg border border-border bg-input/40 px-3 py-2.5 space-y-2">
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
<span className="shrink-0 text-content-muted">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useDashboard, type DashboardRange } from "../../hooks/useDashboard";
|
||||
import { useCurrency } from "../../hooks/useCurrency";
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { fmtInt, fmtPct, fmtTime, fmtTokens } from "../../lib/dashboard-format";
|
||||
import { DailyBars, modelColor } from "./DailyBars";
|
||||
import { DailyBars } from "./DailyBars";
|
||||
import { DailyDetailModal } from "./DailyDetailModal";
|
||||
import { Heatmap } from "./Heatmap";
|
||||
import { TrendLineChart } from "./TrendLineChart";
|
||||
@@ -62,11 +63,6 @@ export function DashboardPage() {
|
||||
const [heatmapSelected, setHeatmapSelected] = useState<DailyRow | null>(null);
|
||||
|
||||
const daily = data?.stats.daily ?? [];
|
||||
const detailDates = useMemo(() => new Set(daily.map((d) => d.date)), [daily]);
|
||||
const heatmapColorMap = useMemo(() => {
|
||||
const unique = [...new Set(daily.flatMap((d) => Object.keys(d.byModel || {})))];
|
||||
return Object.fromEntries(unique.map((m, i) => [m, modelColor(i)]));
|
||||
}, [daily]);
|
||||
|
||||
const RANGE_LABELS: Record<DashboardRange, string> = {
|
||||
today: t("rangeToday"),
|
||||
@@ -109,9 +105,21 @@ export function DashboardPage() {
|
||||
const rangeTotals = data.rangeTotals;
|
||||
const visibleModels = showAllModels ? models : models.slice(0, 8);
|
||||
|
||||
const onHeatmapCellDoubleClick = (cell: HeatmapCell) => {
|
||||
const row = daily.find((d) => d.date === cell.date);
|
||||
if (row) setHeatmapSelected(row);
|
||||
const onHeatmapCellDoubleClick = async (cell: HeatmapCell) => {
|
||||
try {
|
||||
const row = await invoke<DailyRow | null>("get_day_detail", {
|
||||
date: cell.date,
|
||||
});
|
||||
if (row) {
|
||||
setHeatmapSelected(row);
|
||||
} else {
|
||||
setHeatmapSelected(null);
|
||||
alert(t("dayDetailNoData", { date: cell.date }));
|
||||
}
|
||||
} catch (err) {
|
||||
setHeatmapSelected(null);
|
||||
alert(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
};
|
||||
|
||||
// Recent pagination
|
||||
@@ -221,7 +229,7 @@ export function DashboardPage() {
|
||||
|
||||
{/* Heatmap — full row */}
|
||||
<Card title={t("cardHeatmap")} subtitle={t("cardHeatmapSub")}>
|
||||
<Heatmap heatmap={heatmap} detailDates={detailDates} onCellDoubleClick={onHeatmapCellDoubleClick} />
|
||||
<Heatmap heatmap={heatmap} onCellDoubleClick={onHeatmapCellDoubleClick} />
|
||||
</Card>
|
||||
|
||||
{/* Usage-trend tabs + Model table side by side */}
|
||||
@@ -473,7 +481,7 @@ export function DashboardPage() {
|
||||
<DailyDetailModal
|
||||
day={heatmapSelected}
|
||||
dimension="model"
|
||||
colorMap={heatmapColorMap}
|
||||
colorMap={{}}
|
||||
unknownProviderLabel={t("providerUnknown")}
|
||||
onClose={() => setHeatmapSelected(null)}
|
||||
/>,
|
||||
|
||||
@@ -7,9 +7,7 @@ import type { HeatmapCell, HeatmapData } from "../../types/dashboard";
|
||||
|
||||
interface HeatmapProps {
|
||||
heatmap: HeatmapData;
|
||||
/** Set of dates (YYYY-MM-DD) that have detail data in the current range. */
|
||||
detailDates?: Set<string>;
|
||||
/** Called when a cell with detail data is double-clicked. */
|
||||
/** Called when a cell with data (level > 0) is double-clicked. */
|
||||
onCellDoubleClick?: (cell: HeatmapCell) => void;
|
||||
}
|
||||
|
||||
@@ -34,7 +32,7 @@ interface HoverState {
|
||||
y: number;
|
||||
}
|
||||
|
||||
export function Heatmap({ heatmap, detailDates, onCellDoubleClick }: HeatmapProps) {
|
||||
export function Heatmap({ heatmap, onCellDoubleClick }: HeatmapProps) {
|
||||
const { t, lang } = useTranslation();
|
||||
const cells = heatmap?.cells || [];
|
||||
const monthLabels = heatmap?.monthLabels || [];
|
||||
@@ -176,7 +174,7 @@ export function Heatmap({ heatmap, detailDates, onCellDoubleClick }: HeatmapProp
|
||||
);
|
||||
}
|
||||
const level = cell.level ?? 0;
|
||||
const hasDetail = detailDates ? detailDates.has(cell.date) : level > 0;
|
||||
const hasDetail = level > 0;
|
||||
return (
|
||||
<button
|
||||
key={cell.date}
|
||||
|
||||
Reference in new issue
Block a user