From b2001b160e33d417dfa95c76772dd4038ec34518 Mon Sep 17 00:00:00 2001 From: Nathan Franklin Date: Fri, 23 Jan 2026 14:17:28 -0600 Subject: [PATCH 01/10] Rework login and logout --- client/src/hooks/useToken.ts | 9 ++++++ client/src/pages/Callback.tsx | 5 +-- client/src/pages/Login.tsx | 7 ++++- client/src/pages/Logout.tsx | 11 +++++-- client/src/pages/MainPage.tsx | 59 ++++++++++++++++++++++++++++------- 5 files changed, 74 insertions(+), 17 deletions(-) diff --git a/client/src/hooks/useToken.ts b/client/src/hooks/useToken.ts index 2aaec4c..7b1494c 100644 --- a/client/src/hooks/useToken.ts +++ b/client/src/hooks/useToken.ts @@ -57,6 +57,11 @@ const validateTokenAndGetHost = async (token: string, fallbackHost: string): Pro const now = Date.now() / 1000; if (decodedToken.exp < now) { console.warn('Token is expired'); + + // Clear any stale sessionStorage + sessionStorage.removeItem('access_token'); + sessionStorage.removeItem('expires_at'); + return { token: '', tapisHost: fallbackHost, @@ -73,6 +78,10 @@ const validateTokenAndGetHost = async (token: string, fallbackHost: string): Pro if (!response.ok) { console.warn('Token validation failed:', response.status); + + // Clear any stale sessionStorage + sessionStorage.removeItem('access_token'); + sessionStorage.removeItem('expires_at'); return { token: '', tapisHost: fallbackHost, diff --git a/client/src/pages/Callback.tsx b/client/src/pages/Callback.tsx index 487e05c..03ff664 100644 --- a/client/src/pages/Callback.tsx +++ b/client/src/pages/Callback.tsx @@ -38,8 +38,9 @@ const Callback = () => { // Clear the state sessionStorage.removeItem('oauth_state'); - // Redirect to home - navigate('/'); + // Redirect to original page (or home) + const returnTo = sessionStorage.getItem('oauth_return_to') || '/'; + navigate(returnTo, { replace: true }); } catch (error) { console.error('Authentication error:', error); navigate('/login'); diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index 5b4f6c9..aa6275b 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -1,10 +1,16 @@ import { Button } from 'antd'; +import { useSearchParams } from 'react-router-dom'; import { useConfig } from '../hooks/useConfig'; const Login = () => { const config = useConfig(); + const [searchParams] = useSearchParams(); const handleLogin = () => { + // Store where to return after successful auth + const returnTo = searchParams.get('returnTo') || '/'; + sessionStorage.setItem('oauth_return_to', returnTo); + // Generate a random state parameter for security // a store state in sessionStorage for verification const state = Math.random().toString(36).substring(7); @@ -29,7 +35,6 @@ const Login = () => { return (
-

Image Inferencing Service Login

{ const navigate = useNavigate(); + const [searchParams] = useSearchParams(); useEffect(() => { // Clear all session storage sessionStorage.clear(); + // Check if we know where we want to go to (via returnTo) + const returnTo = searchParams.get('returnTo'); + const loginPath = returnTo ? `/login?returnTo=${encodeURIComponent(returnTo)}` : '/login'; + // Redirect to login - navigate('/login'); - }, [navigate]); + navigate(loginPath); + }, [navigate, searchParams]); return (
{ const navigate = useNavigate(); + const location = useLocation(); const config = useConfig(); - const { data: tokenData, isError, isLoading: tokenLoading } = useToken(); + const { data: tokenData, isError: tokenLoadingError, isLoading: tokenLoading } = useToken(); const { data: models, isLoading: modelsLoading, @@ -23,22 +24,55 @@ export const MainPage = () => { } = useInferenceModel(tokenData?.token ?? '', config.apiBasePath); const [isModalVisible, setIsModalVisible] = useState(false); + const inIFrame = isInIframe(); + useEffect(() => { - if (isError || (!tokenLoading && !tokenData?.isValid)) { - navigate('/login'); + if (!inIFrame && !tokenLoading && !tokenData?.isValid) { + const currentPath = location.pathname + location.search; + navigate(`/login?returnTo=${encodeURIComponent(currentPath)}`); } - }, [isError, tokenData, tokenLoading, navigate]); - - const notInIframe = !isInIframe(); + }, [inIFrame, tokenData, tokenLoading, navigate, location.pathname, location.search]); - if (modelsError) { + if (modelsError || tokenLoadingError) { return ( -
+
Failed to load models: {modelsErrorDetail?.message || 'Unkown Error'}
); } + if (inIFrame && !tokenLoading && !tokenData?.isValid) { + return ( +
+ Session Expired +
+ ); + } + if (tokenLoading || modelsLoading || !models || !tokenData) { return (
{ apiBasePath={config.apiBasePath} /> - {notInIframe && ( + {!inIFrame && (
+
+ )} + setIsModalVisible(false)} + footer={null} + width={600} + bodyStyle={{ background: '#fafafa' }} + > + ( + + + {item} + + )} + /> + + + ); +}; + +export default DemoPage; From e2a6cdf559320a25391700263a9b792cad4db986 Mon Sep 17 00:00:00 2001 From: Nathan Franklin Date: Thu, 29 Jan 2026 16:35:07 -0600 Subject: [PATCH 03/10] Add sensitivty dropdown --- client/src/App.tsx | 11 +- client/src/components/DemoInterface.tsx | 35 ++++- client/src/pages/DemoPage.tsx | 32 ++-- client/src/pages/Login.tsx | 36 ++--- client/src/pages/Logout.tsx | 15 +- client/src/pages/MainPage.tsx | 191 ------------------------ client/src/types/inference.ts | 14 ++ imageinf/inference/clip_base.py | 20 ++- imageinf/inference/clip_models.py | 3 + imageinf/inference/models.py | 7 +- imageinf/inference/processor.py | 38 +++-- imageinf/inference/registry.py | 3 +- imageinf/inference/routes.py | 6 +- imageinf/inference/vit_models.py | 4 + 14 files changed, 149 insertions(+), 266 deletions(-) delete mode 100644 client/src/pages/MainPage.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 80bb65c..68d59f6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,8 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import MainPage from './pages/MainPage'; +import LandingPage from './pages/LandingPage'; import DemoPage from './pages/DemoPage'; +import ClassifyPage from './pages/ClassifyPage'; import Login from './pages/Login'; import Logout from './pages/Logout'; import Callback from './pages/Callback'; @@ -14,11 +15,13 @@ function App() { - } /> - } /> + } /> + } /> + } /> } /> } /> } /> + } /> diff --git a/client/src/components/DemoInterface.tsx b/client/src/components/DemoInterface.tsx index dc04657..de57835 100644 --- a/client/src/components/DemoInterface.tsx +++ b/client/src/components/DemoInterface.tsx @@ -43,6 +43,9 @@ const DemoInterface: React.FC = ({ models, tokenInfo, apiBas const [selectedSet, setSelectedSet] = useState(undefined); const [selectedModel, setSelectedModel] = useState(undefined); + const [selectedSensitivity, setSelectedSensitivity] = useState<'high' | 'medium' | 'low'>( + 'medium' + ); const [aggregatedResults, setAggregatedResults] = useState([]); const inferenceMutation = useInference(tokenInfo.token, apiBasePath); @@ -60,13 +63,20 @@ const DemoInterface: React.FC = ({ models, tokenInfo, apiBas } }, [clipModels, selectedModel]); - // Auto-submit when model AND set are both selected +// Submit inference when: +// - User first selects both a model and image set +// - User changes model, set, or sensitivity after initial selection useEffect(() => { if (selectedModel && selectedSet && currentFiles.length > 0) { - inferenceMutation.mutate({ files: currentFiles, model: selectedModel }); + inferenceMutation.mutate({ + files: currentFiles, + model: selectedModel, + sensitivity: selectedSensitivity, + }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedModel, selectedSet]); + // excluding inferenceMutation from deps to avoid infinite loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedModel, selectedSet, selectedSensitivity, currentFiles]); // Clear results when switching set or model useEffect(() => { @@ -143,6 +153,23 @@ const DemoInterface: React.FC = ({ models, tokenInfo, apiBas /> + + +
Sensitivity
+ + +