mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Update .gitignore, refactor UI components, and enhance model download logic
- Added CLAUDE.local.md to .gitignore to prevent local configuration files from being tracked. - Refactored the App component layout for improved responsiveness and user experience. - Updated ModelDownloadProgress to include a new `isDownloaded` state for better visibility of download status. - Simplified SettingsHome by commenting out unused functions and improving the organization of settings options. - Adjusted PrivacyPanel to ensure analytics settings are correctly evaluated. - Modified various page components to use `min-h-full` for consistent height management across the application.
This commit is contained in:
@@ -38,3 +38,5 @@ src-tauri/runtime-skill-*
|
||||
.ruff_cache
|
||||
.kotlin
|
||||
.cargo
|
||||
|
||||
CLAUDE.local.md
|
||||
|
||||
+1
-1
Submodule skills updated: c4f28c3cf6...21f64b6049
+3
-3
@@ -22,13 +22,13 @@ function App() {
|
||||
<AIProvider>
|
||||
<SkillProvider>
|
||||
<Router>
|
||||
<div className="relative min-h-screen">
|
||||
<div className="pointer-events-none fixed inset-x-0 top-0 flex justify-center z-50">
|
||||
<div className="relative h-screen flex flex-col overflow-hidden">
|
||||
<div className="pointer-events-none flex-shrink-0 flex justify-center z-50">
|
||||
<div className="bg-black w-full px-3 py-1.5 text-[11px] uppercase tracking-[0.18em] text-white/40 text-center">
|
||||
AlphaHuman is in early beta.
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-7">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<AppRoutes />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('profile'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: 'Advanced',
|
||||
description: 'Advanced configuration and developer options',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('advanced'),
|
||||
dangerous: false,
|
||||
},
|
||||
{
|
||||
id: 'encryption',
|
||||
title: 'View Encryption Key',
|
||||
description: 'Access your encryption key for backup purposes',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 7a2 2 0 0 1 2 2m4 0a6 6 0 0 1-7.743 5.743L11 17H9v2H7v2H4a1 1 0 0 1-1-1v-2.586a1 1 0 0 1 .293-.707l5.964-5.964A6 6 0 1 1 21 9z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: handleViewEncryptionKey,
|
||||
dangerous: false,
|
||||
},
|
||||
// {
|
||||
// id: 'profile',
|
||||
// title: 'Profile',
|
||||
// description: 'Update your profile information and preferences',
|
||||
// icon: (
|
||||
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
// <path
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// strokeWidth={2}
|
||||
// d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// onClick: () => navigateToSettings('profile'),
|
||||
// dangerous: false,
|
||||
// },
|
||||
// {
|
||||
// id: 'advanced',
|
||||
// title: 'Advanced',
|
||||
// description: 'Advanced configuration and developer options',
|
||||
// icon: (
|
||||
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
// <path
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// strokeWidth={2}
|
||||
// d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
// />
|
||||
// <path
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// strokeWidth={2}
|
||||
// d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// onClick: () => navigateToSettings('advanced'),
|
||||
// dangerous: false,
|
||||
// },
|
||||
// {
|
||||
// id: 'encryption',
|
||||
// title: 'View Encryption Key',
|
||||
// description: 'Access your encryption key for backup purposes',
|
||||
// icon: (
|
||||
// <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
// <path
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// strokeWidth={2}
|
||||
// d="M15 7a2 2 0 0 1 2 2m4 0a6 6 0 0 1-7.743 5.743L11 17H9v2H7v2H4a1 1 0 0 1-1-1v-2.586a1 1 0 0 1 .293-.707l5.964-5.964A6 6 0 1 1 21 9z"
|
||||
// />
|
||||
// </svg>
|
||||
// ),
|
||||
// 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: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
+2
-2
@@ -37,9 +37,9 @@ const Home = () => {
|
||||
const showUpgradeCTA = currentPlan === 'FREE';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative overflow-hidden">
|
||||
<div className="min-h-full relative">
|
||||
{/* Content overlay */}
|
||||
<div className="relative z-10 min-h-screen flex flex-col">
|
||||
<div className="relative z-10 min-h-full flex flex-col">
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full">
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ const Login = () => {
|
||||
|
||||
if (consumeError) {
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center">
|
||||
<div className="min-h-full relative flex items-center justify-center">
|
||||
<div className="relative z-10 max-w-md w-full mx-4 text-center">
|
||||
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
|
||||
<p className="opacity-90 text-coral mb-4">{consumeError}</p>
|
||||
@@ -55,7 +55,7 @@ const Login = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center">
|
||||
<div className="min-h-full relative flex items-center justify-center">
|
||||
<div className="relative z-10 max-w-md w-full mx-4 text-center">
|
||||
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto mb-4"></div>
|
||||
|
||||
@@ -31,7 +31,7 @@ const Welcome = ({ isWeb }: WelcomeProps) => {
|
||||
const isPreparing = isLoading && downloadProgress === null;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center">
|
||||
<div className="min-h-full relative flex items-center justify-center">
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 max-w-md w-full mx-4 space-y-6">
|
||||
{/* Welcome card */}
|
||||
|
||||
@@ -63,7 +63,7 @@ const Onboarding = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center">
|
||||
<div className="min-h-full relative flex items-center justify-center">
|
||||
<div className="relative z-10 max-w-lg w-full mx-4">
|
||||
<div className="flex justify-center mb-6">
|
||||
<LottieAnimation src={stepAnimations[currentStep - 1]} height={120} width={120} />
|
||||
|
||||
@@ -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<ModelStatus>('model_get_status');
|
||||
console.log('[ModelProvider] Initial status:', status);
|
||||
status = await invoke<ModelStatus>('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<boolean>('model_is_available');
|
||||
console.log('[ModelProvider] Model available:', avail);
|
||||
if (avail) {
|
||||
const status = await invoke<ModelStatus>('model_get_status');
|
||||
dispatch(setModelStatus(status));
|
||||
}
|
||||
console.log('[ModelProvider] Available:', avail);
|
||||
if (!avail || cancelled) return;
|
||||
status = await invoke<ModelStatus>('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<ModelStatus>('model_get_status');
|
||||
console.log('[ModelProvider] Download started, status:', status);
|
||||
dispatch(setModelStatus(status));
|
||||
if (cancelled) return;
|
||||
const finalStatus = await invoke<ModelStatus>('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<ModelStatus>('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}</>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user