refactor(explore): rename misnamed "tag cloud" to "visual clusters"
The visual k-means cluster feature was confusingly named tag_cloud / tagCloud / TagCloud across the whole stack, while the actual tag list is explore_tags — the two were trivially easy to mix up (and did cause confusion). Rename the cluster side to visual_cluster / visualCluster / VisualCluster everywhere: command get_tag_cloud -> get_visual_clusters (+ lib.rs registration and the invoke string), VisualClusterEntry, the store fields/actions/tokens, and the mock backend. Old names are retired rather than reused, so any missed reference fails loudly instead of silently resolving to the wrong concept. The tags side keeps its accurate explore_tags naming, and the user-facing "Tag Cloud" UI label is unchanged. Also rename the SQLite tag_cloud_cache table -> visual_cluster_cache (the old table is dropped during schema setup — it is a disposable cache already invalidated by the clustering version bump) and the TagCloud.tsx component file -> ExploreView.tsx, since it is the Explore container hosting both the cluster and tag views.
This commit is contained in:
+2
-2
@@ -5,7 +5,7 @@ import { BackgroundTasks } from "./components/BackgroundTasks";
|
||||
import { Toolbar } from "./components/Toolbar";
|
||||
import { Gallery } from "./components/Gallery";
|
||||
import { Lightbox } from "./components/Lightbox";
|
||||
import { TagCloud } from "./components/TagCloud";
|
||||
import { ExploreView } from "./components/ExploreView";
|
||||
import { DuplicateFinder } from "./components/DuplicateFinder";
|
||||
import { Timeline } from "./components/Timeline";
|
||||
import { TitleBar } from "./components/TitleBar";
|
||||
@@ -88,7 +88,7 @@ export default function App() {
|
||||
) : activeView === "explore" ? (
|
||||
<>
|
||||
<BackgroundTasks />
|
||||
<TagCloud />
|
||||
<ExploreView />
|
||||
</>
|
||||
) : activeView === "duplicates" ? (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { ExploreMode, ExploreTagEntry, RelatedTagEntry, TagCloudEntry, useGalleryStore } from "../store";
|
||||
import { ExploreMode, ExploreTagEntry, RelatedTagEntry, VisualClusterEntry, useGalleryStore } from "../store";
|
||||
import { FolderScopeDropdown } from "./FolderScopeDropdown";
|
||||
import { ThemedDropdown } from "./ThemedDropdown";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
@@ -43,7 +43,7 @@ function seeded(n: number): number {
|
||||
}
|
||||
|
||||
interface PlacedNode {
|
||||
entry: TagCloudEntry;
|
||||
entry: VisualClusterEntry;
|
||||
index: number;
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -57,7 +57,7 @@ interface PlacedNode {
|
||||
rotateSeed: number;
|
||||
}
|
||||
|
||||
function buildCloud(entries: TagCloudEntry[], containerW: number, containerH: number): PlacedNode[] {
|
||||
function buildCloud(entries: VisualClusterEntry[], containerW: number, containerH: number): PlacedNode[] {
|
||||
if (!entries.length || containerW <= 0 || containerH <= 0) return [];
|
||||
|
||||
const maxCount = Math.max(...entries.map((e) => e.count));
|
||||
@@ -622,13 +622,13 @@ function ExploreLoadingPanel({ mode }: { mode: ExploreMode }) {
|
||||
}
|
||||
|
||||
// Separate component so its useLayoutEffect fires when the canvas is actually
|
||||
// mounted — not at TagCloud mount time when the container may still be hidden
|
||||
// mounted — not at ExploreView mount time when the container may still be hidden
|
||||
// behind a loading state.
|
||||
function ClusterCloud({
|
||||
entries,
|
||||
onOpen,
|
||||
}: {
|
||||
entries: TagCloudEntry[];
|
||||
entries: VisualClusterEntry[];
|
||||
onOpen: (imageIds: number[]) => void;
|
||||
}) {
|
||||
const reducedMotion = useReducedMotion();
|
||||
@@ -968,12 +968,12 @@ function TagManageList({
|
||||
);
|
||||
}
|
||||
|
||||
export function TagCloud() {
|
||||
export function ExploreView() {
|
||||
const exploreMode = useGalleryStore((state) => state.exploreMode);
|
||||
const setExploreMode = useGalleryStore((state) => state.setExploreMode);
|
||||
const tagCloudEntries = useGalleryStore((state) => state.tagCloudEntries);
|
||||
const tagCloudLoading = useGalleryStore((state) => state.tagCloudLoading);
|
||||
const loadTagCloud = useGalleryStore((state) => state.loadTagCloud);
|
||||
const visualClusterEntries = useGalleryStore((state) => state.visualClusterEntries);
|
||||
const visualClusterLoading = useGalleryStore((state) => state.visualClusterLoading);
|
||||
const loadVisualClusters = useGalleryStore((state) => state.loadVisualClusters);
|
||||
const exploreTagEntries = useGalleryStore((state) => state.exploreTagEntries);
|
||||
const exploreTagLoading = useGalleryStore((state) => state.exploreTagLoading);
|
||||
const loadExploreTags = useGalleryStore((state) => state.loadExploreTags);
|
||||
@@ -989,13 +989,13 @@ export function TagCloud() {
|
||||
const handleDeleteTag = async (tag: string) => { await deleteTag(tag); };
|
||||
|
||||
useEffect(() => {
|
||||
if (exploreMode === "visual") void loadTagCloud();
|
||||
if (exploreMode === "visual") void loadVisualClusters();
|
||||
else void loadExploreTags();
|
||||
}, [exploreMode, selectedFolderId, loadTagCloud, loadExploreTags]);
|
||||
}, [exploreMode, selectedFolderId, loadVisualClusters, loadExploreTags]);
|
||||
|
||||
const loading = exploreMode === "visual" ? tagCloudLoading : exploreTagLoading;
|
||||
const hasEntries = exploreMode === "visual" ? tagCloudEntries.length > 0 : exploreTagEntries.length > 0;
|
||||
const entryCount = exploreMode === "visual" ? tagCloudEntries.length : exploreTagEntries.length;
|
||||
const loading = exploreMode === "visual" ? visualClusterLoading : exploreTagLoading;
|
||||
const hasEntries = exploreMode === "visual" ? visualClusterEntries.length > 0 : exploreTagEntries.length > 0;
|
||||
const entryCount = exploreMode === "visual" ? visualClusterEntries.length : exploreTagEntries.length;
|
||||
const visibleTagCount = Math.min(exploreTagEntries.length, TAG_ATLAS_MAX_VISIBLE);
|
||||
|
||||
return (
|
||||
@@ -1070,7 +1070,7 @@ export function TagCloud() {
|
||||
</div>
|
||||
) : exploreMode === "visual" ? (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<ClusterCloud entries={tagCloudEntries} onOpen={showVisualCluster} />
|
||||
<ClusterCloud entries={visualClusterEntries} onOpen={showVisualCluster} />
|
||||
</div>
|
||||
) : manageTags ? (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
@@ -250,8 +250,8 @@ export async function handleMockCommand(cmd: string, payload?: unknown): Promise
|
||||
case "find_similar_images":
|
||||
case "find_similar_by_region":
|
||||
return similarImages(payload);
|
||||
case "get_tag_cloud":
|
||||
return db.scenario === "empty" ? [] : db.tagCloud;
|
||||
case "get_visual_clusters":
|
||||
return db.scenario === "empty" ? [] : db.visualClusters;
|
||||
case "get_explore_tags":
|
||||
return db.scenario === "empty" ? [] : db.exploreTags.slice(0, Number(p.limit ?? 180));
|
||||
case "get_related_tags": {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
ImageRecord,
|
||||
ImageTag,
|
||||
SortOrder,
|
||||
TagCloudEntry,
|
||||
VisualClusterEntry,
|
||||
} from "../store";
|
||||
import type { MockScenario } from "./mockScenarios";
|
||||
import { fixtureMediaPath } from "./mockMedia";
|
||||
@@ -21,7 +21,7 @@ export interface MockDb {
|
||||
albums: Album[];
|
||||
albumImageIds: Record<number, number[]>;
|
||||
tagsByImageId: Record<number, ImageTag[]>;
|
||||
tagCloud: TagCloudEntry[];
|
||||
visualClusters: VisualClusterEntry[];
|
||||
exploreTags: ExploreTagEntry[];
|
||||
backgroundJobs: FolderJobProgress[];
|
||||
duplicateGroups: DuplicateGroup[];
|
||||
@@ -252,7 +252,7 @@ function makeProgress(folders: Folder[], scenario: MockScenario): FolderJobProgr
|
||||
}));
|
||||
}
|
||||
|
||||
function makeTagCloud(images: ImageRecord[]): TagCloudEntry[] {
|
||||
function makeVisualClusters(images: ImageRecord[]): VisualClusterEntry[] {
|
||||
return tags.slice(0, 10).map((_, index) => {
|
||||
const group = images.filter((image) => image.id % (index + 2) === 0).slice(0, 28);
|
||||
const representative = group[0] ?? images[index];
|
||||
@@ -305,7 +305,7 @@ export function createMockDb(scenario: MockScenario): MockDb {
|
||||
albums,
|
||||
albumImageIds,
|
||||
tagsByImageId: makeTags(images, scenario),
|
||||
tagCloud: makeTagCloud(images),
|
||||
visualClusters: makeVisualClusters(images),
|
||||
exploreTags: makeExploreTags(images, scenario),
|
||||
backgroundJobs: makeProgress(folders, scenario),
|
||||
duplicateGroups: makeDuplicateGroups(images, scenario),
|
||||
|
||||
+34
-34
@@ -203,7 +203,7 @@ export interface ImageExif {
|
||||
gps_lon: number | null;
|
||||
}
|
||||
|
||||
export interface TagCloudEntry {
|
||||
export interface VisualClusterEntry {
|
||||
count: number;
|
||||
representative_image_id: number;
|
||||
thumbnail_path: string | null;
|
||||
@@ -376,9 +376,9 @@ interface GalleryState {
|
||||
activeView: ActiveView;
|
||||
exploreMode: ExploreMode;
|
||||
tagManagerOpen: boolean;
|
||||
tagCloudEntries: TagCloudEntry[];
|
||||
tagCloudLoading: boolean;
|
||||
tagCloudFolderId: number | null | undefined; // undefined = never loaded
|
||||
visualClusterEntries: VisualClusterEntry[];
|
||||
visualClusterLoading: boolean;
|
||||
visualClusterFolderId: number | null | undefined; // undefined = never loaded
|
||||
exploreTagEntries: ExploreTagEntry[];
|
||||
exploreTagLoading: boolean;
|
||||
// Cache-freshness key: the folder the loaded tags belong to. Set to undefined
|
||||
@@ -497,7 +497,7 @@ interface GalleryState {
|
||||
setExploreMode: (mode: ExploreMode) => void;
|
||||
setTagManagerOpen: (open: boolean) => void;
|
||||
openTagManager: () => void;
|
||||
loadTagCloud: (options?: { force?: boolean }) => Promise<void>;
|
||||
loadVisualClusters: (options?: { force?: boolean }) => Promise<void>;
|
||||
loadExploreTags: (options?: { force?: boolean }) => Promise<void>;
|
||||
loadRelatedTags: (tag: string) => Promise<RelatedTagEntry[]>;
|
||||
showVisualCluster: (imageIds: number[]) => Promise<void>;
|
||||
@@ -633,7 +633,7 @@ const SIMILAR_DISTANCE_THRESHOLD = 0.24;
|
||||
// similarity, region search). Any new request increments it so a stale response
|
||||
// from a previous collection type cannot overwrite newer results.
|
||||
let galleryRequestToken = 0;
|
||||
let tagCloudRequestToken = 0;
|
||||
let visualClusterRequestToken = 0;
|
||||
let exploreTagRequestToken = 0;
|
||||
let exploreTagRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -878,9 +878,9 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
activeView: "gallery",
|
||||
exploreMode: "visual",
|
||||
tagManagerOpen: false,
|
||||
tagCloudEntries: [],
|
||||
tagCloudLoading: false,
|
||||
tagCloudFolderId: undefined,
|
||||
visualClusterEntries: [],
|
||||
visualClusterLoading: false,
|
||||
visualClusterFolderId: undefined,
|
||||
exploreTagEntries: [],
|
||||
exploreTagLoading: false,
|
||||
exploreTagsFolderId: undefined,
|
||||
@@ -1010,7 +1010,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
await loadFolders();
|
||||
await loadBackgroundJobProgress();
|
||||
// Invalidate tag cloud and explore-tags cache since library content changed.
|
||||
set({ tagCloudFolderId: undefined, tagCloudEntries: [], exploreTagsFolderId: undefined });
|
||||
set({ visualClusterFolderId: undefined, visualClusterEntries: [], exploreTagsFolderId: undefined });
|
||||
// Always refresh the gallery: the removed folder's images may be on screen
|
||||
// (e.g. in All Media), not only when that folder was the active selection.
|
||||
await loadImages(true);
|
||||
@@ -1021,7 +1021,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
await invoke("reindex_folder", { folderId });
|
||||
await loadFolders();
|
||||
// Invalidate tag cloud cache since embeddings will be regenerated
|
||||
set({ tagCloudFolderId: undefined, tagCloudEntries: [] });
|
||||
set({ visualClusterFolderId: undefined, visualClusterEntries: [] });
|
||||
await loadBackgroundJobProgress();
|
||||
},
|
||||
|
||||
@@ -1085,7 +1085,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Explore reloads itself via TagCloud's useEffect on selectedFolderId.
|
||||
// Explore reloads itself via ExploreView's useEffect on selectedFolderId.
|
||||
if (activeView === "explore") return;
|
||||
|
||||
void get().loadImages(true);
|
||||
@@ -1425,33 +1425,33 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
set({ exploreMode: "tags", tagManagerOpen: true, settingsOpen: false });
|
||||
},
|
||||
|
||||
loadTagCloud: async (options) => {
|
||||
const { selectedFolderId, tagCloudFolderId, tagCloudLoading } = get();
|
||||
loadVisualClusters: async (options) => {
|
||||
const { selectedFolderId, visualClusterFolderId, visualClusterLoading } = get();
|
||||
const force = options?.force ?? false;
|
||||
// Skip if already loaded for this folder and not currently loading
|
||||
if (!force && !tagCloudLoading && tagCloudFolderId !== undefined && tagCloudFolderId === selectedFolderId) {
|
||||
if (!force && !visualClusterLoading && visualClusterFolderId !== undefined && visualClusterFolderId === selectedFolderId) {
|
||||
return;
|
||||
}
|
||||
const requestToken = ++tagCloudRequestToken;
|
||||
const requestToken = ++visualClusterRequestToken;
|
||||
// On a real folder switch, drop the previous folder's clusters so the loading
|
||||
// panel shows instead of lingering stale results. A same-folder refresh keeps
|
||||
// them to avoid a flicker when the cache returns instantly.
|
||||
const isFolderSwitch = tagCloudFolderId !== selectedFolderId;
|
||||
const isFolderSwitch = visualClusterFolderId !== selectedFolderId;
|
||||
set({
|
||||
tagCloudLoading: true,
|
||||
tagCloudFolderId: selectedFolderId,
|
||||
...(isFolderSwitch ? { tagCloudEntries: [] } : {}),
|
||||
visualClusterLoading: true,
|
||||
visualClusterFolderId: selectedFolderId,
|
||||
...(isFolderSwitch ? { visualClusterEntries: [] } : {}),
|
||||
});
|
||||
try {
|
||||
const entries = await invoke<TagCloudEntry[]>("get_tag_cloud", {
|
||||
const entries = await invoke<VisualClusterEntry[]>("get_visual_clusters", {
|
||||
folderId: selectedFolderId,
|
||||
});
|
||||
if (requestToken !== tagCloudRequestToken) return;
|
||||
set({ tagCloudEntries: entries, tagCloudLoading: false });
|
||||
if (requestToken !== visualClusterRequestToken) return;
|
||||
set({ visualClusterEntries: entries, visualClusterLoading: false });
|
||||
} catch (error) {
|
||||
if (requestToken !== tagCloudRequestToken) return;
|
||||
if (requestToken !== visualClusterRequestToken) return;
|
||||
console.error("Failed to load tag cloud:", error);
|
||||
set({ tagCloudLoading: false });
|
||||
set({ visualClusterLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2411,13 +2411,13 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
|
||||
renameTag: async (from, to) => {
|
||||
await invoke("rename_tag", { params: { from, to } });
|
||||
// Tag content changed — invalidate the explore-tags and tag-cloud caches.
|
||||
// Tag content changed — invalidate the explore-tags and visual-cluster caches.
|
||||
// Keep the current tag list visible while the refresh runs so manager UI
|
||||
// state such as filtering and sorting is not lost to a loading remount.
|
||||
set({
|
||||
exploreTagsFolderId: undefined,
|
||||
tagCloudFolderId: undefined,
|
||||
tagCloudEntries: [],
|
||||
visualClusterFolderId: undefined,
|
||||
visualClusterEntries: [],
|
||||
});
|
||||
const parsed = parseSearchValue(get().search);
|
||||
if (parsed.mode === "tag" && parsed.query === from) {
|
||||
@@ -2435,8 +2435,8 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
// state such as filtering and sorting is not lost to a loading remount.
|
||||
set({
|
||||
exploreTagsFolderId: undefined,
|
||||
tagCloudFolderId: undefined,
|
||||
tagCloudEntries: [],
|
||||
visualClusterFolderId: undefined,
|
||||
visualClusterEntries: [],
|
||||
});
|
||||
const parsed = parseSearchValue(get().search);
|
||||
if (parsed.mode === "tag" && parsed.query === tag) {
|
||||
@@ -2507,14 +2507,14 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
if (ids.length === 0 || cleaned.length === 0) return;
|
||||
await invoke<void>("bulk_add_tags", { params: { image_ids: ids, tags: cleaned } });
|
||||
// New tags landed — invalidate Explore tag caches.
|
||||
set({ exploreTagsFolderId: undefined, tagCloudFolderId: undefined, tagCloudEntries: [] });
|
||||
set({ exploreTagsFolderId: undefined, visualClusterFolderId: undefined, visualClusterEntries: [] });
|
||||
},
|
||||
|
||||
bulkRemoveTag: async (tag) => {
|
||||
const ids = Array.from(get().gallerySelectedIds);
|
||||
if (ids.length === 0 || !tag.trim()) return;
|
||||
await invoke<void>("bulk_remove_tag", { params: { image_ids: ids, tag: tag.trim() } });
|
||||
set({ exploreTagsFolderId: undefined, tagCloudFolderId: undefined, tagCloudEntries: [] });
|
||||
set({ exploreTagsFolderId: undefined, visualClusterFolderId: undefined, visualClusterEntries: [] });
|
||||
},
|
||||
|
||||
bulkDeleteSelected: async () => {
|
||||
@@ -2532,8 +2532,8 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
totalImages: Math.max(0, state.totalImages - succeededIds.length),
|
||||
gallerySelectedIds: new Set([...state.gallerySelectedIds].filter((id) => !succeededSet.has(id))),
|
||||
// Deletion changes tag/duplicate/album aggregates.
|
||||
tagCloudFolderId: undefined,
|
||||
tagCloudEntries: [],
|
||||
visualClusterFolderId: undefined,
|
||||
visualClusterEntries: [],
|
||||
exploreTagsFolderId: undefined,
|
||||
}));
|
||||
// The DB cascade already removed these from album_images; refresh counts/covers.
|
||||
|
||||
Reference in New Issue
Block a user