fix: improve moments clustering

This commit is contained in:
William Valentin
2026-02-05 09:14:45 -08:00
parent fdd1c932fd
commit 523460f639
4 changed files with 255 additions and 0 deletions

View File

@@ -27,6 +27,17 @@ type ApiTreeResponse = {
nodes: ApiTreeRow[];
};
type MomentCluster = {
day: string;
count: number;
};
type MomentsResponse = {
start: string | null;
end: string | null;
clusters: MomentCluster[];
};
type Orientation = "vertical" | "horizontal";
type ExpandedState = Record<string, boolean>;
@@ -147,6 +158,9 @@ export function TimelineTree(props: {
const [expanded, setExpanded] = useState<ExpandedState>({});
const [rows, setRows] = useState<ApiTreeRow[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [showMoments, setShowMoments] = useState(false);
const [moments, setMoments] = useState<MomentsResponse | null>(null);
const [momentsError, setMomentsError] = useState<string | null>(null);
// simple pan/zoom via viewBox
const svgRef = useRef<SVGSVGElement | null>(null);
@@ -182,6 +196,34 @@ export function TimelineTree(props: {
};
}, []);
useEffect(() => {
if (!showMoments || !rows) return;
let cancelled = false;
async function loadMoments() {
try {
setMomentsError(null);
if (!rows || rows.length === 0) return;
const start = rows[0]?.group_ts ?? null;
const end = rows[rows.length - 1]?.group_ts ?? null;
const params = new URLSearchParams();
if (start) params.set("start", start);
if (end) params.set("end", end);
const res = await fetch(`/api/moments?${params.toString()}`, {
cache: "no-store",
});
if (!res.ok) throw new Error(`moments_fetch_failed:${res.status}`);
const json = (await res.json()) as MomentsResponse;
if (!cancelled) setMoments(json);
} catch (e) {
if (!cancelled) setMomentsError(e instanceof Error ? e.message : String(e));
}
}
void loadMoments();
return () => {
cancelled = true;
};
}, [showMoments, rows]);
const roots = useMemo(() => (rows ? buildHierarchy(rows) : []), [rows]);
const visible = useMemo(
() => gatherVisible(roots, expanded),
@@ -315,12 +357,18 @@ export function TimelineTree(props: {
>
Reset view
</button>
<button type="button" onClick={() => setShowMoments((v) => !v)}>
{showMoments ? "Hide moments" : "Show moments"}
</button>
{rows ? (
<span style={{ color: "#666" }}>{rows.length} day nodes</span>
) : null}
</div>
{error ? <div style={{ color: "#b00" }}>Error: {error}</div> : null}
{momentsError ? (
<div style={{ color: "#b00" }}>Moments error: {momentsError}</div>
) : null}
{!rows && !error ? (
<div
style={{
@@ -390,6 +438,15 @@ export function TimelineTree(props: {
const isDay = node.id.startsWith("d:");
const clickCursor = hasChildren || isDay ? "pointer" : "default";
const dayKey = node.label;
const dayMoments = showMoments
? moments?.clusters.filter((c) => c.day === dayKey) ?? []
: [];
const momentsCount = dayMoments.reduce(
(sum, c) => sum + c.count,
0,
);
return (
<g
key={node.id}
@@ -404,6 +461,7 @@ export function TimelineTree(props: {
<text x={20} y={5} fontSize={14} fill="#111">
{node.label} ({node.countReady}/{node.countTotal})
{hasChildren ? (isExpanded ? " ▼" : " ▶") : ""}
{showMoments && isDay ? ` · ${momentsCount} moment assets` : ""}
</text>
</g>
);