- {/* Query Image */}
- {queryImage && (
-
-

- {isSearchActive && (
-
- )}
-
- )}
+ useEffect(() => {
+ if (clustersSuccess && clustersData?.data?.clusters) {
+ const clusters = (clustersData.data.clusters || []) as Cluster[];
+ dispatch(setClusters(clusters));
+ }
+ }, [clustersData, clustersSuccess, dispatch]);
- {/* Input */}
-
+ const handlePersonClick = (clusterId: string) => {
+ navigate(`/person/${clusterId}`);
+ setIsFocused(false);
+ };
- {/* FaceSearch Dialog */}
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') setIsFocused(false);
+ if (e.key === 'Enter' && isFocused && data.trim()) {
+ handleSearch();
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [data, isFocused]);
-
+ const getDateSuggestions = (input: string): string[] => {
+ const clean = input.toLowerCase().trim();
+ if (!clean) return [];
-
+ const months = [
+ 'january',
+ 'february',
+ 'march',
+ 'april',
+ 'may',
+ 'june',
+ 'july',
+ 'august',
+ 'september',
+ 'october',
+ 'november',
+ 'december',
+ ];
+
+ const currentYear = new Date().getFullYear();
+ const years = [currentYear, currentYear - 1, currentYear - 2];
+ const suggestions: string[] = [];
+
+ // 1. Pehle face names check karo
+ const matchedFaceNames = faceNames.filter((n) => {
+ if (!n) return false;
+ return n.toLowerCase().includes(clean);
+ });
+
+ // 2. Phir months check karo
+ const matchedMonths = months.filter((month) => month.startsWith(clean));
+
+ // Face names ko suggestions mein add karo
+ matchedFaceNames.forEach((name) => {
+ suggestions.push(name);
+ });
+
+ // Date suggestions add karo
+ matchedMonths.forEach((month) => {
+ years.forEach((year) => {
+ suggestions.push(`${month} ${year}`);
+ });
+ });
+ return suggestions.slice(0, 12);
+ };
+
+ const handleSuggestionClick = (suggestion: string) => {
+ if (!suggestion) return;
+ setData(suggestion);
+ navigate(`/search/${encodeURIComponent(suggestion)}`);
+ setIsFocused(false);
+ };
+
+ // Search button handler
+ const handleSearch = () => {
+ if (!data.trim()) return;
+ console.log(`/search/${encodeURIComponent(data)}`);
+ navigate(`/search/${encodeURIComponent(data)}`);
+ setIsFocused(false);
+ };
+
+ // facename handling
+ const [faceNames, setFaceNames] = useState
([]);
+ const dateSuggestions = getDateSuggestions(data); // Isme ab face names bhi honge
+ const hasPartialMatch = dateSuggestions.length > 0;
+
+ useEffect(() => {
+ if (clusters && clusters.length > 0) {
+ const names = clusters.map((cluster: any) => cluster.cluster_name);
+ setFaceNames(names);
+ // console.log('Face Names:', names);
+ }
+ }, [clusters]);
+ return (
+ <>
+
- {/* Right Side */}
-
-
-
+
+ {/* Right Side */}
+
+
+
+
+
+ Welcome {userName}
+
+
+
+
+
-
+
+ {/* info about the search bar if user not clicks in the face i will show the date suggestion */}
+ {isFocused && data === '' && (
+ <>
+
setIsFocused(false)}
+ />
+
e.stopPropagation()}
+ >
+
+
+ Search events
+
+
+
+ {[
+ 'Beach trip',
+ 'Marriage',
+ 'Office',
+ 'Birthday',
+ 'Graduation',
+ ].map((item) => (
+ handleSuggestionClick(item)}
+ >
+ {item}
+
+ ))}
+
+
+ Faces
+
+ {clusters.length === 0 ? (
+ <>
+
+ No faces found. PictoPy will automatically detect and group
+ faces as you add more photos.
+
+ >
+ ) : (
+ <>
+
+ {clusters.map((cluster: any) => (
+
handlePersonClick(cluster.cluster_id)}
+ >
+
+
+
+ {cluster.cluster_name?.charAt(0).toUpperCase() ||
+ cluster.cluster_id.charAt(0).toUpperCase()}
+
+
+
+
+ {cluster.cluster_name ||
+ `Person ${cluster.cluster_id.slice(-4)}`}
+
+
+ {cluster.face_count} photo
+ {cluster.face_count !== 1 ? 's' : ''}
+
+
+
+ ))}
+
+ >
+ )}
+
+ >
+ )}
+
+ {isFocused && data !== '' && (
+ <>
+
setIsFocused(false)}
+ />
+
e.stopPropagation()}
+ >
+ {hasPartialMatch ? (
+ <>
+
+
+ Search by date , name and more...
+
+
+ {dateSuggestions.map((suggestion) => (
+
handleSuggestionClick(suggestion)}
+ >
+
+ {suggestion}
+
+
+ ))}
+
+
+ >
+ ) : (
+ <>
+
+
+
+ No matching dates found
+
+
+ Try searching for a month name (e.g., "January 2025")
+
+
+
+
+
+
+ Search for "{data}"
+
+
+ Search in all photos
+
+
+
+
+
+
+ >
+ )}
+
+ >
+ )}
+ >
);
}
diff --git a/frontend/src/components/OnboardingSteps/ServerCheck.tsx b/frontend/src/components/OnboardingSteps/ServerCheck.tsx
index 94962caa6..7d2448456 100644
--- a/frontend/src/components/OnboardingSteps/ServerCheck.tsx
+++ b/frontend/src/components/OnboardingSteps/ServerCheck.tsx
@@ -22,7 +22,7 @@ export const ServerCheck: React.FC
= ({ stepIndex }) => {
} = usePictoQuery({
queryKey: ['clusters'],
queryFn: getMainBackendHealthStatus,
- retry: 10,
+ retry: 60,
retryDelay: 1000,
});
const {
@@ -32,7 +32,7 @@ export const ServerCheck: React.FC = ({ stepIndex }) => {
} = usePictoQuery({
queryKey: ['syncMicroservice'],
queryFn: getSyncMicroserviceHealthStatus,
- retry: 10,
+ retry: 60,
retryDelay: 1000,
});
useEffect(() => {
diff --git a/frontend/src/config/Backend.ts b/frontend/src/config/Backend.ts
index 28b16c48f..2013d2d15 100644
--- a/frontend/src/config/Backend.ts
+++ b/frontend/src/config/Backend.ts
@@ -1,2 +1,2 @@
-export const BACKEND_URL = 'http://localhost:8000';
-export const SYNC_MICROSERVICE_URL = 'http://localhost:8001/api/v1';
+export const BACKEND_URL = 'http://localhost:52123';
+export const SYNC_MICROSERVICE_URL = 'http://localhost:52124';
diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts
index 7a8da5bb5..4662902a3 100644
--- a/frontend/src/constants/routes.ts
+++ b/frontend/src/constants/routes.ts
@@ -9,4 +9,5 @@ export const ROUTES = {
ALBUMS: 'albums',
MEMORIES: 'memories',
PERSON: 'person/:clusterId',
+ SEARCH: 'search/:query',
};
diff --git a/frontend/src/features/searchSlice.ts b/frontend/src/features/searchSlice.ts
index 9786277c1..7e8372b07 100644
--- a/frontend/src/features/searchSlice.ts
+++ b/frontend/src/features/searchSlice.ts
@@ -1,12 +1,15 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { Image } from '@tauri-apps/api/image';
interface SearchState {
active: boolean;
+ images: Image[];
queryImage?: string;
}
const initialState: SearchState = {
active: false,
+ images: [],
queryImage: undefined,
};
diff --git a/frontend/src/hooks/useFolderOperations.tsx b/frontend/src/hooks/useFolderOperations.tsx
index 0c0fcc559..ff747ff0b 100644
--- a/frontend/src/hooks/useFolderOperations.tsx
+++ b/frontend/src/hooks/useFolderOperations.tsx
@@ -101,8 +101,7 @@ export const useFolderOperations = () => {
useMutationFeedback(enableAITaggingMutation, {
showLoading: true,
loadingMessage: 'Enabling AI tagging',
- successTitle: 'AI Tagging Enabled',
- successMessage: 'AI tagging has been enabled for the selected folder.',
+ showSuccess: false,
errorTitle: 'AI Tagging Error',
errorMessage: 'Failed to enable AI tagging. Please try again.',
});
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index c76c26065..e01868d7c 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client';
import App from './App';
import BrowserWarning from './components/BrowserWarning';
import { isProd } from './utils/isProd';
-import { stopServer, startServer } from './utils/serverUtils';
+import { startServer } from './utils/serverUtils';
import { isTauriEnvironment } from './utils/tauriUtils';
import { store } from './app/store';
import { Provider } from 'react-redux';
@@ -18,7 +18,7 @@ const onCloseListener = async () => {
try {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
await getCurrentWindow().onCloseRequested(async () => {
- await stopServer();
+ // code to stop the server
});
} catch (error) {
console.error('Error setting up close listener:', error);
diff --git a/frontend/src/pages/PersonImages/PersonImages.tsx b/frontend/src/pages/PersonImages/PersonImages.tsx
index ea646b8ad..9a4ddce08 100644
--- a/frontend/src/pages/PersonImages/PersonImages.tsx
+++ b/frontend/src/pages/PersonImages/PersonImages.tsx
@@ -44,6 +44,7 @@ export const PersonImages = () => {
setClusterName(res?.cluster_name || 'random_name');
dispatch(hideLoader());
}
+ console.log(images);
}, [data, isSuccess, isError, isLoading, dispatch]);
const handleEditName = () => {
diff --git a/frontend/src/pages/SearchImages/SearchImages.tsx b/frontend/src/pages/SearchImages/SearchImages.tsx
new file mode 100644
index 000000000..402fd14e4
--- /dev/null
+++ b/frontend/src/pages/SearchImages/SearchImages.tsx
@@ -0,0 +1,192 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { Image } from '@/types/Media';
+import { setImages } from '@/features/imageSlice';
+import { showLoader, hideLoader } from '@/features/loaderSlice';
+import { selectImages } from '@/features/imageSelectors';
+import { usePictoQuery } from '@/hooks/useQueryExtension';
+import { fetchAllClusters, fetchAllImages } from '@/api/api-functions';
+import { RootState } from '@/app/store';
+import { showInfoDialog } from '@/features/infoDialogSlice';
+import { useNavigate, useParams } from 'react-router';
+import { setClusters } from '@/features/faceClustersSlice';
+import { Cluster } from '@/types/Media';
+import { Button } from '@/components/ui/button';
+import { ArrowLeft } from 'lucide-react';
+import { ROUTES } from '@/constants/routes';
+import {
+ ChronologicalGallery,
+ MonthMarker,
+} from '@/components/Media/ChronologicalGallery';
+import { EmptyGalleryState } from '@/components/EmptyStates/EmptyGalleryState';
+import TimelineScrollbar from '@/components/Timeline/TimelineScrollbar';
+
+export const SearchImages = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const images = useSelector(selectImages);
+ const searchState = useSelector((state: RootState) => state.search);
+ const { clusters } = useSelector((state: RootState) => state.faceClusters);
+ const query = useParams().query || '';
+
+ const { data: clustersData, isSuccess: clustersSuccess } = usePictoQuery({
+ queryKey: ['clusters'],
+ queryFn: fetchAllClusters,
+ });
+
+ // Check if query is a face name and redirect
+ useEffect(() => {
+ if (clusters && clusters.length > 0 && query) {
+ // Find cluster with exact name match
+ const matchedCluster = clusters.find(
+ (cluster: Cluster) =>
+ cluster.cluster_name?.toLowerCase().trim() ===
+ query.toLowerCase().trim(),
+ );
+
+ if (matchedCluster) {
+ // Query is a face name, redirect to person page
+ navigate(`/person/${matchedCluster.cluster_id}`);
+ return;
+ }
+ }
+ }, [clusters, query, navigate]);
+
+ useEffect(() => {
+ if (clustersSuccess && clustersData?.data?.clusters) {
+ const clusters = (clustersData.data.clusters || []) as Cluster[];
+ dispatch(setClusters(clusters));
+ }
+ }, [clustersData, clustersSuccess, dispatch]);
+
+ const isSearchActive = searchState.active;
+ const searchResults = searchState.images;
+
+ const handleMonthOffsetsChange = useCallback((entries: MonthMarker[]) => {
+ setMonthMarkers((prev) => {
+ if (
+ prev.length === entries.length &&
+ prev.every(
+ (m, i) =>
+ m.offset === entries[i].offset &&
+ m.month === entries[i].month &&
+ m.year === entries[i].year,
+ )
+ ) {
+ return prev;
+ }
+ return entries;
+ });
+ }, []);
+
+ const fetchAllImagesWrapper = async ({
+ queryKey,
+ }: {
+ queryKey: [string, boolean?];
+ }) => {
+ const [, tagged] = queryKey;
+ return fetchAllImages(tagged);
+ };
+ const [monthMarkers, setMonthMarkers] = useState([]);
+ const { data, isLoading, isSuccess, isError } = usePictoQuery({
+ queryKey: ['images'],
+ queryFn: fetchAllImagesWrapper,
+ enabled: !isSearchActive, // Fixed typo here
+ });
+
+ useEffect(() => {
+ if (!isSearchActive) {
+ if (isLoading) {
+ dispatch(showLoader('Loading images'));
+ } else if (isError) {
+ dispatch(hideLoader());
+ dispatch(
+ showInfoDialog({
+ title: 'Error',
+ message: 'Failed to load images. Please try again later.',
+ variant: 'error',
+ }),
+ );
+ } else if (isSuccess) {
+ const images = data?.data as Image[];
+ dispatch(setImages(images));
+ dispatch(hideLoader());
+ }
+ }
+ }, [data, isSuccess, isError, isLoading, dispatch, isSearchActive]);
+
+ // Filter by month
+ const filterImagesByMonthYear = (
+ images: Image[],
+ monthYearString: string | null,
+ ) => {
+ if (!monthYearString) return images;
+ return images.filter((img) => {
+ if (!img.metadata?.date_created) return false;
+ const dateCreated = new Date(img.metadata.date_created);
+ const imgMonthYear = dateCreated.toLocaleDateString('en-US', {
+ month: 'long',
+ year: 'numeric',
+ });
+ return imgMonthYear === monthYearString;
+ });
+ };
+ const scrollableRef = useRef(null);
+
+ // Format query
+ const selectedMonthYear = query
+ ? query
+ .split(' ')
+ .map(
+ (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
+ )
+ .join(' ')
+ : null;
+ const displayImages = filterImagesByMonthYear(images, selectedMonthYear);
+ const title = selectedMonthYear
+ ? `${selectedMonthYear} (${displayImages.length} images)`
+ : isSearchActive && searchResults.length > 0
+ ? `Face Search Results (${searchResults.length} found)`
+ : 'All Images';
+
+ return (
+
+ {/* Gallery Section */}
+
+
+ {displayImages.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Timeline Scrollbar */}
+ {monthMarkers.length > 0 && (
+
+ )}
+
+ );
+};
diff --git a/frontend/src/pages/SettingsPage/components/ApplicationControlsCard.tsx b/frontend/src/pages/SettingsPage/components/ApplicationControlsCard.tsx
index 69c9abaa0..bcecd3f08 100644
--- a/frontend/src/pages/SettingsPage/components/ApplicationControlsCard.tsx
+++ b/frontend/src/pages/SettingsPage/components/ApplicationControlsCard.tsx
@@ -1,16 +1,10 @@
import React, { useState } from 'react';
-import {
- Settings as SettingsIcon,
- RefreshCw,
- Server,
- Users,
-} from 'lucide-react';
+import { Settings as SettingsIcon, RefreshCw, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';
import UpdateDialog from '@/components/Updater/UpdateDialog';
import SettingsCard from './SettingsCard';
-import { restartServer } from '@/utils/serverUtils';
import { useUpdater } from '@/hooks/useUpdater';
import { useDispatch } from 'react-redux';
import { showLoader, hideLoader } from '@/features/loaderSlice';
@@ -140,17 +134,6 @@ const ApplicationControlsCard: React.FC = () => {
-
-