From d3d50c92be9cd3848f866f7d17bd06cca44ed22f Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Tue, 12 May 2026 18:40:33 +0800 Subject: [PATCH] fix: update image info panel when navigating between files Info panel always read files[0] and stored results in a single useState, so it never changed when switching images. Now tracks selectedIndex, auto-fetches on navigation after initial "Read Info" click, caches results per-index, and aborts stale requests on rapid navigation. --- .../src/components/tools/info-settings.tsx | 100 +++++++++++++----- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/apps/web/src/components/tools/info-settings.tsx b/apps/web/src/components/tools/info-settings.tsx index 93e9a85b..1b05ebef 100644 --- a/apps/web/src/components/tools/info-settings.tsx +++ b/apps/web/src/components/tools/info-settings.tsx @@ -1,5 +1,5 @@ import { Loader2 } from "lucide-react"; -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { formatHeaders } from "@/lib/api"; import { useFileStore } from "@/stores/file-store"; @@ -32,37 +32,83 @@ interface ImageInfoData { export function InfoSettings() { const { files, processing, error, setProcessing, setError } = useFileStore(); + const selectedIndex = useFileStore((s) => s.selectedIndex); const [info, setInfo] = useState(null); + const cacheRef = useRef>(new Map()); + const autoFetchRef = useRef(false); + const abortRef = useRef(null); - const handleProcess = async () => { - if (files.length === 0) return; + const fetchInfo = useCallback( + async (index: number) => { + const file = useFileStore.getState().entries[index]?.file; + if (!file) return; - setProcessing(true); - setError(null); - setInfo(null); - - try { - const formData = new FormData(); - formData.append("file", files[0]); - - const res = await fetch("/api/v1/tools/info", { - method: "POST", - headers: formatHeaders(), - body: formData, - }); - - if (!res.ok) { - const body = await res.json().catch(() => ({})); - throw new Error(body.error || `Failed: ${res.status}`); + const cached = cacheRef.current.get(index); + if (cached) { + setInfo(cached); + return; } - const data: ImageInfoData = await res.json(); - setInfo(data); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to read info"); - } finally { - setProcessing(false); - } + abortRef.current?.abort(); + const controller = new AbortController(); + abortRef.current = controller; + + setProcessing(true); + setError(null); + + try { + const formData = new FormData(); + formData.append("file", file); + + const res = await fetch("/api/v1/tools/info", { + method: "POST", + headers: formatHeaders(), + body: formData, + signal: controller.signal, + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `Failed: ${res.status}`); + } + + const data: ImageInfoData = await res.json(); + cacheRef.current.set(index, data); + setInfo(data); + } catch (err) { + if (err instanceof DOMException && err.name === "AbortError") return; + setError(err instanceof Error ? err.message : "Failed to read info"); + } finally { + if (!controller.signal.aborted) { + setProcessing(false); + } + } + }, + [setProcessing, setError], + ); + + useEffect(() => { + cacheRef.current.clear(); + autoFetchRef.current = false; + setInfo(null); + }, [files.length]); + + useEffect(() => { + if (!autoFetchRef.current || files.length === 0) return; + fetchInfo(selectedIndex); + }, [selectedIndex, fetchInfo, files.length]); + + useEffect(() => { + return () => { + abortRef.current?.abort(); + }; + }, []); + + const handleProcess = () => { + if (files.length === 0) return; + autoFetchRef.current = true; + setInfo(null); + fetchInfo(selectedIndex); }; const hasFile = files.length > 0;