fix: improve moments clustering
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user