diff --git a/.gitignore b/.gitignore index bb66f93d6..4d6c60b6a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ src-tauri/runtime-skill-* .ruff_cache .kotlin .cargo + +CLAUDE.local.md diff --git a/skills b/skills index c4f28c3cf..21f64b604 160000 --- a/skills +++ b/skills @@ -1 +1 @@ -Subproject commit c4f28c3cf615517de395d70a0793ac0086071162 +Subproject commit 21f64b604930a91e4639ae1d7618a75444ffb072 diff --git a/src/App.tsx b/src/App.tsx index 9b2bca24e..63751d9a5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,13 +22,13 @@ function App() { -
-
+
+
AlphaHuman is in early beta.
-
+
diff --git a/src/components/ModelDownloadProgress.tsx b/src/components/ModelDownloadProgress.tsx index 4ad68e1fc..666948e34 100644 --- a/src/components/ModelDownloadProgress.tsx +++ b/src/components/ModelDownloadProgress.tsx @@ -12,7 +12,7 @@ const ModelDownloadProgress = ({ className = '', showWhenLoaded = false, }: ModelDownloadProgressProps) => { - const { isAvailable, isLoaded, isLoading, downloadProgress, error, ensureLoaded } = + const { isAvailable, isLoaded, isLoading, isDownloaded, downloadProgress, error, ensureLoaded } = useModelStatus(); const [isMobile, setIsMobile] = useState(false); @@ -63,8 +63,8 @@ const ModelDownloadProgress = ({ return null; } - // Don't render if loaded and showWhenLoaded is false - if (isLoaded && !showWhenLoaded && !isLoading) { + // Hide when downloaded or loaded (nothing for the user to do) + if ((isDownloaded || isLoaded) && !showWhenLoaded && !isLoading) { return null; } diff --git a/src/components/settings/SettingsHome.tsx b/src/components/settings/SettingsHome.tsx index fa52a7271..6b9cbffa6 100644 --- a/src/components/settings/SettingsHome.tsx +++ b/src/components/settings/SettingsHome.tsx @@ -13,10 +13,10 @@ const SettingsHome = () => { closeSettings(); }; - const handleViewEncryptionKey = () => { - // TODO: Show encryption key in a secure modal - console.log('View encryption key'); - }; + // const handleViewEncryptionKey = () => { + // // TODO: Show encryption key in a secure modal + // console.log('View encryption key'); + // }; const handleDeleteAllData = () => { // TODO: Show confirmation dialog and delete all data @@ -64,63 +64,63 @@ const SettingsHome = () => { onClick: () => navigateToSettings('privacy'), dangerous: false, }, - { - id: 'profile', - title: 'Profile', - description: 'Update your profile information and preferences', - icon: ( - - - - ), - onClick: () => navigateToSettings('profile'), - dangerous: false, - }, - { - id: 'advanced', - title: 'Advanced', - description: 'Advanced configuration and developer options', - icon: ( - - - - - ), - onClick: () => navigateToSettings('advanced'), - dangerous: false, - }, - { - id: 'encryption', - title: 'View Encryption Key', - description: 'Access your encryption key for backup purposes', - icon: ( - - - - ), - onClick: handleViewEncryptionKey, - dangerous: false, - }, + // { + // id: 'profile', + // title: 'Profile', + // description: 'Update your profile information and preferences', + // icon: ( + // + // + // + // ), + // onClick: () => navigateToSettings('profile'), + // dangerous: false, + // }, + // { + // id: 'advanced', + // title: 'Advanced', + // description: 'Advanced configuration and developer options', + // icon: ( + // + // + // + // + // ), + // onClick: () => navigateToSettings('advanced'), + // dangerous: false, + // }, + // { + // id: 'encryption', + // title: 'View Encryption Key', + // description: 'Access your encryption key for backup purposes', + // icon: ( + // + // + // + // ), + // onClick: handleViewEncryptionKey, + // dangerous: false, + // }, { id: 'team', title: 'Team', @@ -140,7 +140,7 @@ const SettingsHome = () => { }, { id: 'billing', - title: 'Billing', + title: 'Billing & Usage', description: 'Manage your subscription and payment methods', icon: ( diff --git a/src/components/settings/panels/PrivacyPanel.tsx b/src/components/settings/panels/PrivacyPanel.tsx index 9d2d145d9..8656b74dc 100644 --- a/src/components/settings/panels/PrivacyPanel.tsx +++ b/src/components/settings/panels/PrivacyPanel.tsx @@ -11,7 +11,7 @@ const PrivacyPanel = () => { const analyticsEnabled = useAppSelector(state => { const userId = state.user.user?._id; if (!userId) return false; - return state.auth.isAnalyticsEnabledByUser[userId] === true; + return state.auth.isAnalyticsEnabledByUser[userId] !== false; }); const handleToggleAnalytics = () => { diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index d22151e68..a2bd44149 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -37,9 +37,9 @@ const Home = () => { const showUpgradeCTA = currentPlan === 'FREE'; return ( -
+
{/* Content overlay */} -
+
{/* Main content */}
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 7abb58d48..1f0a95514 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -41,7 +41,7 @@ const Login = () => { if (consumeError) { return ( -
+

{consumeError}

@@ -55,7 +55,7 @@ const Login = () => { } return ( -
+
diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx index 72103f89f..ae9901f35 100644 --- a/src/pages/Welcome.tsx +++ b/src/pages/Welcome.tsx @@ -31,7 +31,7 @@ const Welcome = ({ isWeb }: WelcomeProps) => { const isPreparing = isLoading && downloadProgress === null; return ( -
+
{/* Main content */}
{/* Welcome card */} diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index f916cf5fe..173faa12f 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -63,7 +63,7 @@ const Onboarding = () => { }; return ( -
+
diff --git a/src/providers/ModelProvider.tsx b/src/providers/ModelProvider.tsx index e97dd85ea..1d39d7297 100644 --- a/src/providers/ModelProvider.tsx +++ b/src/providers/ModelProvider.tsx @@ -1,6 +1,6 @@ import { invoke } from '@tauri-apps/api/core'; import { platform } from '@tauri-apps/plugin-os'; -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import { @@ -19,74 +19,67 @@ const POLL_INTERVAL = 1000; */ const ModelProvider = ({ children }: { children: React.ReactNode }) => { const dispatch = useAppDispatch(); - const { loading, downloaded, available, downloadTriggered } = useAppSelector( - state => state.model - ); - const pollingRef = useRef(false); - const initDone = useRef(false); + const loading = useAppSelector(state => state.model.loading); + const downloadTriggered = useAppSelector(state => state.model.downloadTriggered); - // Initial status fetch + availability check (runs once) + // Single init effect: fetch status → check platform → auto-download if needed. + // No ref guard — safe to re-run; Rust backend prevents concurrent downloads. useEffect(() => { - if (initDone.current) return; - initDone.current = true; + let cancelled = false; const init = async () => { - console.log('[ModelProvider] Initializing...'); + // 1. Fetch initial status + let status: ModelStatus; try { - const status = await invoke('model_get_status'); - console.log('[ModelProvider] Initial status:', status); + status = await invoke('model_get_status'); + console.log('[ModelProvider] Initial status:', JSON.stringify(status)); + if (cancelled) return; dispatch(setModelStatus(status)); } catch (err) { - console.log('[ModelProvider] model_get_status failed (non-Tauri?):', err); - return; // Not a Tauri environment, nothing more to do + console.log('[ModelProvider] Not in Tauri environment:', err); + return; } + // 2. Check availability try { const avail = await invoke('model_is_available'); - console.log('[ModelProvider] Model available:', avail); - if (avail) { - const status = await invoke('model_get_status'); - dispatch(setModelStatus(status)); - } + console.log('[ModelProvider] Available:', avail); + if (!avail || cancelled) return; + status = await invoke('model_get_status'); + if (cancelled) return; + dispatch(setModelStatus(status)); } catch (err) { - console.log('[ModelProvider] model_is_available failed:', err); + console.log('[ModelProvider] Availability check failed:', err); + return; } - }; - init(); - }, [dispatch]); - // Auto-trigger download on desktop when model is available but not downloaded - useEffect(() => { - if (downloadTriggered) { - console.log('[ModelProvider] Auto-download: already triggered, skipping'); - return; - } - if (!available) { - console.log('[ModelProvider] Auto-download: not available yet, waiting'); - return; - } - if (downloaded) { - console.log('[ModelProvider] Auto-download: already downloaded, skipping'); - return; - } - if (loading) { - console.log('[ModelProvider] Auto-download: already loading, skipping'); - return; - } + // 3. If already downloaded or already loading, nothing to do + if (status.downloaded) { + console.log('[ModelProvider] Already downloaded, skipping auto-download'); + return; + } + if (status.loading) { + console.log('[ModelProvider] Already loading, will poll'); + if (!cancelled) dispatch(setModelLoading(true)); + return; + } - const tryAutoDownload = async () => { + // 4. Check platform — only auto-download on desktop try { const currentPlatform = await platform(); console.log('[ModelProvider] Platform:', currentPlatform); if (currentPlatform === 'android' || currentPlatform === 'ios') { - console.log('[ModelProvider] Mobile platform, skipping auto-download'); + console.log('[ModelProvider] Mobile platform, skipping'); return; } } catch (err) { - console.log('[ModelProvider] Platform detection failed (likely web), skipping:', err); + console.log('[ModelProvider] Platform detection failed (web?), skipping:', err); return; } + if (cancelled) return; + + // 5. Start download console.log('[ModelProvider] Starting auto-download...'); dispatch(setDownloadTriggered(true)); dispatch(setModelLoading(true)); @@ -94,43 +87,48 @@ const ModelProvider = ({ children }: { children: React.ReactNode }) => { try { await invoke('model_start_download'); - const status = await invoke('model_get_status'); - console.log('[ModelProvider] Download started, status:', status); - dispatch(setModelStatus(status)); + if (cancelled) return; + const finalStatus = await invoke('model_get_status'); + console.log('[ModelProvider] Download complete:', JSON.stringify(finalStatus)); + if (!cancelled) dispatch(setModelStatus(finalStatus)); } catch (err) { - console.error('[ModelProvider] Auto-download failed:', err); - dispatch(setModelError(err instanceof Error ? err.message : 'Failed to download model')); + console.error('[ModelProvider] Download failed:', err); + if (!cancelled) + dispatch(setModelError(err instanceof Error ? err.message : String(err))); } }; - tryAutoDownload(); - }, [dispatch, downloadTriggered, available, downloaded, loading]); + // Only run if download hasn't been triggered yet (Redux state, survives StrictMode) + if (!downloadTriggered) { + init(); + } + + return () => { + cancelled = true; + }; + }, [dispatch, downloadTriggered]); // Poll status while loading/downloading useEffect(() => { - if (!loading) { - pollingRef.current = false; - return; - } - - pollingRef.current = true; - console.log('[ModelProvider] Polling started (loading=true)'); + if (!loading) return; + console.log('[ModelProvider] Polling started'); const interval = setInterval(async () => { - if (!pollingRef.current) return; try { const status = await invoke('model_get_status'); dispatch(setModelStatus(status)); if (!status.loading) { - console.log('[ModelProvider] Polling stopped (loading done)', status); - pollingRef.current = false; + console.log('[ModelProvider] Loading finished:', JSON.stringify(status)); } } catch { - // ignore polling errors + // ignore } }, POLL_INTERVAL); - return () => clearInterval(interval); + return () => { + console.log('[ModelProvider] Polling stopped'); + clearInterval(interval); + }; }, [dispatch, loading]); return <>{children}; diff --git a/src/services/analytics.ts b/src/services/analytics.ts index 49cc0daae..c361de1bb 100644 --- a/src/services/analytics.ts +++ b/src/services/analytics.ts @@ -30,7 +30,7 @@ export function isAnalyticsEnabled(): boolean { const state = store.getState(); const userId = state.user?.user?._id; if (!userId) return false; - return state.auth.isAnalyticsEnabledByUser[userId] === true; + return state.auth.isAnalyticsEnabledByUser[userId] !== false; } // ---------------------------------------------------------------------------