Polish search and embedding UX

- add semantic text search with toolbar mode switching and sqlite-vec query support
- improve embedding progress visibility, failure recovery, and similar-image affordances
- add search clearing and keyboard controls for filename vs semantic search modes
- refine background task interactions and gallery/lightbox embedding states

Refs: #4
This commit is contained in:
2026-04-06 01:45:25 +01:00
parent 51e4c2c1f7
commit c6a66d1ba9
16 changed files with 978 additions and 415 deletions
+51 -2
View File
@@ -14,6 +14,7 @@ export interface Folder {
export type MediaKind = "image" | "video";
export type MediaFilter = "all" | MediaKind;
export type ZoomPreset = "compact" | "comfortable" | "detail";
export type SearchMode = "filename" | "semantic";
export interface ImageRecord {
id: number;
@@ -77,7 +78,9 @@ export type SortOrder =
| "name_asc"
| "name_desc"
| "size_desc"
| "size_asc";
| "size_asc"
| "duration_desc"
| "duration_asc";
interface GalleryState {
folders: Folder[];
@@ -87,6 +90,7 @@ interface GalleryState {
loadedCount: number;
loadingImages: boolean;
search: string;
searchMode: SearchMode;
sort: SortOrder;
mediaFilter: MediaFilter;
favoritesOnly: boolean;
@@ -106,6 +110,9 @@ interface GalleryState {
loadImages: (reset?: boolean) => Promise<void>;
loadMoreImages: () => Promise<void>;
setSearch: (search: string) => void;
clearSearch: () => void;
resetSearch: () => void;
setSearchMode: (mode: SearchMode) => void;
setSort: (sort: SortOrder) => void;
setMediaFilter: (filter: MediaFilter) => void;
setFavoritesOnly: (favoritesOnly: boolean) => void;
@@ -171,6 +178,10 @@ function compareImages(a: ImageRecord, b: ImageRecord, sort: SortOrder): number
return compareNullableNumber(a.file_size, b.file_size);
case "size_desc":
return compareNullableNumber(b.file_size, a.file_size);
case "duration_asc":
return compareNullableNumber(a.duration_ms, b.duration_ms);
case "duration_desc":
return compareNullableNumber(b.duration_ms, a.duration_ms);
default:
return compareNullableDate(b.modified_at, a.modified_at);
}
@@ -237,6 +248,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
loadedCount: 0,
loadingImages: false,
search: "",
searchMode: "filename",
sort: "date_desc",
mediaFilter: "all",
favoritesOnly: false,
@@ -292,10 +304,31 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
},
loadImages: async (reset = false) => {
const { selectedFolderId, search, sort, loadedCount, mediaFilter, favoritesOnly } = get();
const { selectedFolderId, search, searchMode, sort, loadedCount, mediaFilter, favoritesOnly } = get();
set({ loadingImages: true });
try {
if (searchMode === "semantic" && search.trim()) {
const images = await invoke<ImageRecord[]>("semantic_search_images", {
params: {
query: search,
folder_id: selectedFolderId,
media_kind: mediaFilter === "all" ? null : mediaFilter,
favorites_only: favoritesOnly,
limit: PAGE_SIZE,
},
});
set({
images,
totalImages: images.length,
loadedCount: images.length,
loadingImages: false,
collectionTitle: `Semantic search: ${search}`,
});
return;
}
const offset = reset ? 0 : loadedCount;
const result = await invoke<{
images: ImageRecord[];
@@ -338,6 +371,21 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
void get().loadImages(true);
},
clearSearch: () => {
set({ search: "", images: [], loadedCount: 0, collectionTitle: null });
void get().loadImages(true);
},
resetSearch: () => {
set({ search: "", searchMode: "filename", images: [], loadedCount: 0, collectionTitle: null });
void get().loadImages(true);
},
setSearchMode: (searchMode) => {
set({ searchMode, images: [], loadedCount: 0, collectionTitle: null });
void get().loadImages(true);
},
setSort: (sort) => {
set({ sort, images: [], loadedCount: 0, collectionTitle: null });
void get().loadImages(true);
@@ -369,6 +417,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
loadingImages: false,
collectionTitle: "Similar Images",
selectedFolderId: null,
selectedImage: null,
});
},