mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Enhance model download functionality and UI feedback
- Added a new `downloaded` status to the `ModelStatus` interface to track if the model file has been downloaded. - Implemented `startDownload` function in the `useModelStatus` hook to initiate model downloads without loading into memory. - Updated the `Welcome` component to automatically trigger model downloads on desktop and provide visual feedback on download progress. - Enhanced the Tauri backend with a new `model_start_download` command to support the download process. - Improved the model status management to reflect the current download state and handle errors effectively.
This commit is contained in:
+1
-1
Submodule skills updated: 3112738083...c4f28c3cf6
@@ -21,6 +21,8 @@ pub struct ModelStatusResponse {
|
||||
pub loaded: bool,
|
||||
/// Whether the model is currently being loaded or downloaded.
|
||||
pub loading: bool,
|
||||
/// Whether the model file has been downloaded to disk.
|
||||
pub downloaded: bool,
|
||||
/// Download progress (0.0 to 1.0) if downloading.
|
||||
pub download_progress: Option<f32>,
|
||||
/// Error message if loading failed.
|
||||
@@ -135,6 +137,7 @@ pub fn model_get_status() -> ModelStatusResponse {
|
||||
available: status.available,
|
||||
loaded: status.loaded,
|
||||
loading: status.loading,
|
||||
downloaded: status.downloaded,
|
||||
download_progress: status.download_progress,
|
||||
error: status.error,
|
||||
model_path: status.model_path,
|
||||
@@ -149,6 +152,7 @@ pub fn model_get_status() -> ModelStatusResponse {
|
||||
available: true,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
downloaded: false,
|
||||
download_progress: None,
|
||||
error: Some("MediaPipe LLM: Download a model to get started".to_string()),
|
||||
model_path: None,
|
||||
@@ -161,6 +165,7 @@ pub fn model_get_status() -> ModelStatusResponse {
|
||||
available: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
downloaded: false,
|
||||
download_progress: None,
|
||||
error: Some("Model not available on iOS".to_string()),
|
||||
model_path: None,
|
||||
@@ -248,6 +253,28 @@ pub async fn model_summarize(text: String, max_tokens: Option<u32>) -> Result<St
|
||||
}
|
||||
}
|
||||
|
||||
/// Start downloading the model file without loading into memory.
|
||||
/// Safe to call from the Welcome page (pre-auth). Supports resume.
|
||||
#[tauri::command]
|
||||
pub async fn model_start_download() -> Result<(), String> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
crate::services::llama::LLAMA_MANAGER
|
||||
.download_only()
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
Err("Use Android model manager to download models".to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
Err("Model not available on iOS".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Unload the model from memory to free resources.
|
||||
#[tauri::command]
|
||||
pub fn model_unload() -> Result<(), String> {
|
||||
|
||||
@@ -461,6 +461,7 @@ pub fn run() {
|
||||
model_is_available,
|
||||
model_get_status,
|
||||
model_ensure_loaded,
|
||||
model_start_download,
|
||||
model_generate,
|
||||
model_summarize,
|
||||
model_unload,
|
||||
@@ -553,6 +554,7 @@ pub fn run() {
|
||||
model_is_available,
|
||||
model_get_status,
|
||||
model_ensure_loaded,
|
||||
model_start_download,
|
||||
model_generate,
|
||||
model_summarize,
|
||||
model_unload,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Provides:
|
||||
//! - Lazy model loading on first use
|
||||
//! - Automatic model download if not present
|
||||
//! - Resumable downloads with HTTP Range support
|
||||
//! - Thread-safe inference with dedicated thread pool
|
||||
//! - Generate and summarize API for skills
|
||||
|
||||
@@ -33,6 +34,13 @@ const MODEL_URL: &str = "https://huggingface.co/bartowski/google_gemma-3n-E2B-it
|
||||
/// Expected SHA256 hash for model verification (first 16 chars for quick check)
|
||||
const MODEL_SHA256_PREFIX: &str = ""; // Will be verified on first download
|
||||
|
||||
/// Sidecar metadata for resumable downloads.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DownloadMeta {
|
||||
total_size: u64,
|
||||
url: String,
|
||||
}
|
||||
|
||||
/// Status of the local model.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -43,6 +51,8 @@ pub struct ModelStatus {
|
||||
pub loaded: bool,
|
||||
/// Whether the model is currently being loaded or downloaded.
|
||||
pub loading: bool,
|
||||
/// Whether the model file has been downloaded to disk.
|
||||
pub downloaded: bool,
|
||||
/// Download progress (0.0 to 1.0) if downloading.
|
||||
pub download_progress: Option<f32>,
|
||||
/// Error message if loading failed.
|
||||
@@ -57,6 +67,7 @@ impl Default for ModelStatus {
|
||||
available: true,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
downloaded: false,
|
||||
download_progress: None,
|
||||
error: None,
|
||||
model_path: None,
|
||||
@@ -109,6 +120,8 @@ pub struct LlamaManager {
|
||||
status: RwLock<ModelStatus>,
|
||||
/// Lock to prevent concurrent loading.
|
||||
loading: AtomicBool,
|
||||
/// Lock to prevent concurrent downloads.
|
||||
downloading: AtomicBool,
|
||||
}
|
||||
|
||||
impl LlamaManager {
|
||||
@@ -119,6 +132,7 @@ impl LlamaManager {
|
||||
model: RwLock::new(None),
|
||||
status: RwLock::new(ModelStatus::default()),
|
||||
loading: AtomicBool::new(false),
|
||||
downloading: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,14 +141,19 @@ impl LlamaManager {
|
||||
log::info!("[llama] Setting data dir: {:?}", dir);
|
||||
*self.data_dir.write() = dir.clone();
|
||||
|
||||
// Update status with model path
|
||||
// Update status with model path and downloaded flag
|
||||
let model_path = dir.join(MODEL_FILENAME);
|
||||
self.status.write().model_path = Some(model_path.to_string_lossy().to_string());
|
||||
let mut status = self.status.write();
|
||||
status.model_path = Some(model_path.to_string_lossy().to_string());
|
||||
status.downloaded = model_path.exists();
|
||||
}
|
||||
|
||||
/// Get the current model status.
|
||||
pub fn get_status(&self) -> ModelStatus {
|
||||
self.status.read().clone()
|
||||
let mut status = self.status.read().clone();
|
||||
// Dynamically check downloaded state
|
||||
status.downloaded = self.model_exists();
|
||||
status
|
||||
}
|
||||
|
||||
/// Get the model file path.
|
||||
@@ -142,14 +161,87 @@ impl LlamaManager {
|
||||
self.data_dir.read().join(MODEL_FILENAME)
|
||||
}
|
||||
|
||||
/// Get the temp download file path.
|
||||
fn download_path(&self) -> PathBuf {
|
||||
self.model_path().with_extension("gguf.download")
|
||||
}
|
||||
|
||||
/// Get the download metadata sidecar file path.
|
||||
fn meta_path(&self) -> PathBuf {
|
||||
self.model_path().with_extension("gguf.download.meta")
|
||||
}
|
||||
|
||||
/// Check if the model file exists.
|
||||
fn model_exists(&self) -> bool {
|
||||
self.model_path().exists()
|
||||
}
|
||||
|
||||
/// Download the model from HuggingFace.
|
||||
/// Download the model file without loading into memory.
|
||||
/// Safe to call from the Welcome page (pre-auth).
|
||||
pub async fn download_only(&self) -> Result<(), String> {
|
||||
// Already downloaded
|
||||
if self.model_exists() {
|
||||
log::info!("[llama] Model already downloaded, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.download_model().await
|
||||
}
|
||||
|
||||
/// Download the model from HuggingFace with resume support.
|
||||
async fn download_model(&self) -> Result<(), String> {
|
||||
// Already downloaded
|
||||
if self.model_exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Prevent concurrent downloads — second caller waits
|
||||
if self.downloading.swap(true, Ordering::SeqCst) {
|
||||
log::info!("[llama] Download already in progress, waiting...");
|
||||
while self.downloading.load(Ordering::SeqCst) {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
// Check if the other download succeeded
|
||||
if self.model_exists() {
|
||||
return Ok(());
|
||||
}
|
||||
return Err("Download failed (another attempt)".to_string());
|
||||
}
|
||||
|
||||
// Update status
|
||||
{
|
||||
let mut status = self.status.write();
|
||||
status.loading = true;
|
||||
status.error = None;
|
||||
}
|
||||
|
||||
let result = self.download_model_inner().await;
|
||||
|
||||
// Update status based on result
|
||||
{
|
||||
let mut status = self.status.write();
|
||||
status.loading = false;
|
||||
status.download_progress = None;
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
status.downloaded = true;
|
||||
status.error = None;
|
||||
}
|
||||
Err(e) => {
|
||||
status.error = Some(e.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.downloading.store(false, Ordering::SeqCst);
|
||||
result
|
||||
}
|
||||
|
||||
/// Inner download logic with HTTP Range resume support.
|
||||
async fn download_model_inner(&self) -> Result<(), String> {
|
||||
let model_path = self.model_path();
|
||||
let mut download_path = self.download_path();
|
||||
let mut meta_path = self.meta_path();
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = model_path.parent() {
|
||||
@@ -157,33 +249,120 @@ impl LlamaManager {
|
||||
.map_err(|e| format!("Failed to create model directory: {}", e))?;
|
||||
}
|
||||
|
||||
log::info!("[llama] Downloading model from {}", MODEL_URL);
|
||||
self.status.write().download_progress = Some(0.0);
|
||||
// Check for existing partial download
|
||||
let (mut existing_size, mut resume) =
|
||||
self.check_resume_state(&download_path, &meta_path);
|
||||
|
||||
// Use reqwest for download
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(MODEL_URL)
|
||||
|
||||
// Build request — add Range header if resuming
|
||||
let mut request = client.get(MODEL_URL);
|
||||
if resume && existing_size > 0 {
|
||||
log::info!(
|
||||
"[llama] Resuming download from byte {}",
|
||||
existing_size
|
||||
);
|
||||
request = request.header("Range", format!("bytes={}-", existing_size));
|
||||
}
|
||||
|
||||
let mut response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to start download: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Download failed with status: {}", response.status()));
|
||||
let mut status_code = response.status().as_u16();
|
||||
|
||||
// If we got 206 but the file size changed, delete temp files and start fresh
|
||||
if status_code == 206 {
|
||||
let total = self.parse_content_range_total(&response, existing_size);
|
||||
let meta_total = self.read_meta_total(&meta_path);
|
||||
if let Some(mt) = meta_total {
|
||||
if mt != total {
|
||||
log::warn!(
|
||||
"[llama] Remote file size changed ({} vs {}), restarting download",
|
||||
mt,
|
||||
total
|
||||
);
|
||||
let _ = std::fs::remove_file(&download_path);
|
||||
let _ = std::fs::remove_file(&meta_path);
|
||||
|
||||
// Re-issue a fresh GET (no Range header)
|
||||
response = client
|
||||
.get(MODEL_URL)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to restart download: {}", e))?;
|
||||
status_code = response.status().as_u16();
|
||||
existing_size = 0;
|
||||
let _ = resume; // no longer resuming
|
||||
|
||||
// Re-read paths (unchanged, but reset state)
|
||||
download_path = self.download_path();
|
||||
meta_path = self.meta_path();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total_size = response.content_length().unwrap_or(0);
|
||||
let mut downloaded: u64 = 0;
|
||||
// Determine download mode based on response
|
||||
let (mut file, start_offset, total_size) = match status_code {
|
||||
206 => {
|
||||
// Partial Content — resume accepted
|
||||
let total = self.parse_content_range_total(&response, existing_size);
|
||||
|
||||
// Create temp file for download
|
||||
let temp_path = model_path.with_extension("download");
|
||||
let mut file = std::fs::File::create(&temp_path)
|
||||
.map_err(|e| format!("Failed to create temp file: {}", e))?;
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&download_path)
|
||||
.map_err(|e| format!("Failed to open temp file for append: {}", e))?;
|
||||
|
||||
log::info!(
|
||||
"[llama] Resuming download: {}/{} bytes",
|
||||
existing_size,
|
||||
total
|
||||
);
|
||||
|
||||
(file, existing_size, total)
|
||||
}
|
||||
200 => {
|
||||
// OK — server ignored Range or fresh start
|
||||
let total = response.content_length().unwrap_or(0);
|
||||
|
||||
// Start fresh — truncate any partial download
|
||||
let file = std::fs::File::create(&download_path)
|
||||
.map_err(|e| format!("Failed to create temp file: {}", e))?;
|
||||
|
||||
// Write meta for future resume
|
||||
self.write_meta(&meta_path, total)?;
|
||||
|
||||
log::info!("[llama] Starting fresh download: {} bytes", total);
|
||||
|
||||
(file, 0u64, total)
|
||||
}
|
||||
416 => {
|
||||
// Range Not Satisfiable — file is already complete
|
||||
log::info!("[llama] Download already complete (416), renaming");
|
||||
let _ = std::fs::remove_file(&meta_path);
|
||||
std::fs::rename(&download_path, &model_path)
|
||||
.map_err(|e| format!("Failed to rename temp file: {}", e))?;
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Download failed with status: {}", status_code));
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial progress
|
||||
if total_size > 0 {
|
||||
let progress = start_offset as f32 / total_size as f32;
|
||||
self.status.write().download_progress = Some(progress);
|
||||
} else {
|
||||
self.status.write().download_progress = Some(0.0);
|
||||
}
|
||||
|
||||
// Stream the download
|
||||
use futures::StreamExt;
|
||||
use std::io::Write;
|
||||
let mut stream = response.bytes_stream();
|
||||
use futures::StreamExt;
|
||||
let mut downloaded = start_offset;
|
||||
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.map_err(|e| format!("Download error: {}", e))?;
|
||||
@@ -197,7 +376,10 @@ impl LlamaManager {
|
||||
self.status.write().download_progress = Some(progress);
|
||||
|
||||
// Log progress every 10%
|
||||
if (progress * 10.0) as u32 > ((downloaded - chunk.len() as u64) as f32 / total_size as f32 * 10.0) as u32 {
|
||||
let prev_pct =
|
||||
((downloaded - chunk.len() as u64) as f32 / total_size as f32 * 10.0) as u32;
|
||||
let curr_pct = (progress * 10.0) as u32;
|
||||
if curr_pct > prev_pct {
|
||||
log::info!("[llama] Download progress: {:.1}%", progress * 100.0);
|
||||
}
|
||||
}
|
||||
@@ -208,16 +390,136 @@ impl LlamaManager {
|
||||
.map_err(|e| format!("Failed to flush file: {}", e))?;
|
||||
drop(file);
|
||||
|
||||
// Rename temp file to final path
|
||||
std::fs::rename(&temp_path, &model_path)
|
||||
// Clean up meta file and rename temp → final
|
||||
let _ = std::fs::remove_file(&meta_path);
|
||||
std::fs::rename(&download_path, &model_path)
|
||||
.map_err(|e| format!("Failed to rename temp file: {}", e))?;
|
||||
|
||||
log::info!("[llama] Model downloaded successfully to {:?}", model_path);
|
||||
self.status.write().download_progress = None;
|
||||
log::info!(
|
||||
"[llama] Model downloaded successfully to {:?}",
|
||||
model_path
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if we can resume a previous partial download.
|
||||
/// Returns (existing_bytes, should_resume).
|
||||
fn check_resume_state(&self, download_path: &PathBuf, meta_path: &PathBuf) -> (u64, bool) {
|
||||
let file_exists = download_path.exists();
|
||||
let meta_exists = meta_path.exists();
|
||||
|
||||
if !file_exists {
|
||||
// No partial download — start fresh
|
||||
if meta_exists {
|
||||
let _ = std::fs::remove_file(meta_path);
|
||||
}
|
||||
return (0, false);
|
||||
}
|
||||
|
||||
// Get existing file size
|
||||
let existing_size = match std::fs::metadata(download_path) {
|
||||
Ok(m) => m.len(),
|
||||
Err(_) => {
|
||||
let _ = std::fs::remove_file(download_path);
|
||||
let _ = std::fs::remove_file(meta_path);
|
||||
return (0, false);
|
||||
}
|
||||
};
|
||||
|
||||
if existing_size == 0 {
|
||||
let _ = std::fs::remove_file(download_path);
|
||||
let _ = std::fs::remove_file(meta_path);
|
||||
return (0, false);
|
||||
}
|
||||
|
||||
// Validate meta file
|
||||
if !meta_exists {
|
||||
// No meta — can't verify, start fresh
|
||||
log::warn!("[llama] Partial download exists but no meta file, restarting");
|
||||
let _ = std::fs::remove_file(download_path);
|
||||
return (0, false);
|
||||
}
|
||||
|
||||
// Read and validate meta
|
||||
match std::fs::read_to_string(meta_path) {
|
||||
Ok(contents) => match serde_json::from_str::<DownloadMeta>(&contents) {
|
||||
Ok(meta) => {
|
||||
if meta.url != MODEL_URL {
|
||||
log::warn!("[llama] Meta URL mismatch, restarting download");
|
||||
let _ = std::fs::remove_file(download_path);
|
||||
let _ = std::fs::remove_file(meta_path);
|
||||
return (0, false);
|
||||
}
|
||||
if existing_size >= meta.total_size && meta.total_size > 0 {
|
||||
// Download was complete but rename didn't happen — handle this
|
||||
log::info!(
|
||||
"[llama] Partial download appears complete ({}/{})",
|
||||
existing_size,
|
||||
meta.total_size
|
||||
);
|
||||
}
|
||||
(existing_size, true)
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!("[llama] Corrupt meta file, restarting download");
|
||||
let _ = std::fs::remove_file(download_path);
|
||||
let _ = std::fs::remove_file(meta_path);
|
||||
(0, false)
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
log::warn!("[llama] Cannot read meta file, restarting download");
|
||||
let _ = std::fs::remove_file(download_path);
|
||||
let _ = std::fs::remove_file(meta_path);
|
||||
(0, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the total size from a Content-Range header (e.g., "bytes 1000-2000/5000").
|
||||
fn parse_content_range_total(
|
||||
&self,
|
||||
response: &reqwest::Response,
|
||||
fallback_offset: u64,
|
||||
) -> u64 {
|
||||
if let Some(range) = response.headers().get("content-range") {
|
||||
if let Ok(range_str) = range.to_str() {
|
||||
// Format: "bytes START-END/TOTAL"
|
||||
if let Some(slash_pos) = range_str.rfind('/') {
|
||||
if let Ok(total) = range_str[slash_pos + 1..].trim().parse::<u64>() {
|
||||
// Write meta if we don't have it yet
|
||||
let _ = self.write_meta(&self.meta_path(), total);
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: content-length + offset
|
||||
let content_len = response.content_length().unwrap_or(0);
|
||||
fallback_offset + content_len
|
||||
}
|
||||
|
||||
/// Read the total_size from an existing meta file.
|
||||
fn read_meta_total(&self, meta_path: &PathBuf) -> Option<u64> {
|
||||
let contents = std::fs::read_to_string(meta_path).ok()?;
|
||||
let meta: DownloadMeta = serde_json::from_str(&contents).ok()?;
|
||||
Some(meta.total_size)
|
||||
}
|
||||
|
||||
/// Write download metadata sidecar file.
|
||||
fn write_meta(&self, meta_path: &PathBuf, total_size: u64) -> Result<(), String> {
|
||||
let meta = DownloadMeta {
|
||||
total_size,
|
||||
url: MODEL_URL.to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&meta)
|
||||
.map_err(|e| format!("Failed to serialize meta: {}", e))?;
|
||||
std::fs::write(meta_path, json)
|
||||
.map_err(|e| format!("Failed to write meta file: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure the model is loaded into memory.
|
||||
pub async fn ensure_loaded(&self) -> Result<(), String> {
|
||||
// Already loaded?
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface ModelStatus {
|
||||
available: boolean;
|
||||
loaded: boolean;
|
||||
loading: boolean;
|
||||
downloaded: boolean;
|
||||
downloadProgress: number | null;
|
||||
error: string | null;
|
||||
modelPath: string | null;
|
||||
@@ -17,6 +18,7 @@ const DEFAULT_STATUS: ModelStatus = {
|
||||
available: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
downloaded: false,
|
||||
downloadProgress: null,
|
||||
error: null,
|
||||
modelPath: null,
|
||||
@@ -57,6 +59,25 @@ export const useModelStatus = (pollInterval = 1000) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Start downloading the model without loading into memory
|
||||
const startDownload = useCallback(async () => {
|
||||
try {
|
||||
setStatus(prev => ({ ...prev, loading: true, error: null }));
|
||||
setIsPolling(true);
|
||||
await invoke('model_start_download');
|
||||
await fetchStatus();
|
||||
setIsPolling(false);
|
||||
} catch (error) {
|
||||
console.error('[useModelStatus] Failed to start download:', error);
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to download model',
|
||||
}));
|
||||
setIsPolling(false);
|
||||
}
|
||||
}, [fetchStatus]);
|
||||
|
||||
// Start loading/downloading the model
|
||||
const ensureLoaded = useCallback(async () => {
|
||||
try {
|
||||
@@ -115,8 +136,10 @@ export const useModelStatus = (pollInterval = 1000) => {
|
||||
isAvailable: status.available,
|
||||
isLoaded: status.loaded,
|
||||
isLoading: status.loading,
|
||||
isDownloaded: status.downloaded,
|
||||
downloadProgress: status.downloadProgress,
|
||||
error: status.error,
|
||||
startDownload,
|
||||
ensureLoaded,
|
||||
unload,
|
||||
refresh: fetchStatus,
|
||||
|
||||
+61
-8
@@ -1,6 +1,9 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import DownloadScreen from '../components/DownloadScreen';
|
||||
import TelegramLoginButton from '../components/TelegramLoginButton';
|
||||
import TypewriterGreeting from '../components/TypewriterGreeting';
|
||||
import { useModelStatus } from '../hooks/useModelStatus';
|
||||
|
||||
interface WelcomeProps {
|
||||
isWeb: boolean;
|
||||
@@ -11,14 +14,31 @@ const Welcome = ({ isWeb }: WelcomeProps) => {
|
||||
'Hello HAL9000! 👋',
|
||||
"Let's cook! 🔥",
|
||||
'The A-Team is here! 👊',
|
||||
// "Welcome to the exclusive club of crypto degenerates! 🎪🚀",
|
||||
// "Let's get you richer than a Nigerian prince's email! 👑💸",
|
||||
// "Ready to HODL like your life depends on it? 🤝💀",
|
||||
// "Welcome, future crypto millionaire (results not guaranteed)! 🎰💎",
|
||||
// "Time to make Wall Street bros jealous AF! 📈🔥",
|
||||
// "Ready to go to the moon? Pack light! 🌙🚀"
|
||||
];
|
||||
|
||||
const { isAvailable, isDownloaded, isLoading, downloadProgress, error, startDownload } =
|
||||
useModelStatus();
|
||||
const downloadTriggered = useRef(false);
|
||||
|
||||
// Auto-trigger model download on desktop
|
||||
useEffect(() => {
|
||||
if (isWeb || !isAvailable || isDownloaded || isLoading || downloadTriggered.current) return;
|
||||
downloadTriggered.current = true;
|
||||
startDownload();
|
||||
}, [isWeb, isAvailable, isDownloaded, isLoading, startDownload]);
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
downloadTriggered.current = false;
|
||||
startDownload();
|
||||
}, [startDownload]);
|
||||
|
||||
const progressPercent = downloadProgress !== null ? Math.round(downloadProgress * 100) : 0;
|
||||
|
||||
// Determine what to show for download progress
|
||||
const showProgress = !isWeb && isAvailable && !isDownloaded;
|
||||
const isDownloading = isLoading && downloadProgress !== null;
|
||||
const isPreparing = isLoading && downloadProgress === null;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center">
|
||||
{/* Main content */}
|
||||
@@ -28,8 +48,6 @@ const Welcome = ({ isWeb }: WelcomeProps) => {
|
||||
{/* Greeting */}
|
||||
<TypewriterGreeting greetings={greetings} />
|
||||
|
||||
{/* <br /> */}
|
||||
|
||||
<p className="opacity-70 mb-8 leading-relaxed">
|
||||
Welcome to AlphaHuman. Your Telegram assistant here to get you 10x more done in your
|
||||
journey.
|
||||
@@ -37,6 +55,41 @@ const Welcome = ({ isWeb }: WelcomeProps) => {
|
||||
|
||||
<p className="opacity-70 leading-relaxed">Are you ready for this?</p>
|
||||
|
||||
{/* Model download progress (desktop only) */}
|
||||
{showProgress && (
|
||||
<div className="mt-6">
|
||||
{isDownloading && (
|
||||
<div className="space-y-2">
|
||||
<div className="w-full bg-stone-700/30 rounded-full h-1.5 overflow-hidden">
|
||||
<div
|
||||
className="bg-primary-500/80 h-full rounded-full transition-all duration-300 ease-out"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs opacity-50">
|
||||
Downloading AI model... {progressPercent}%
|
||||
<span className="ml-1 opacity-70">(~1.2 GB)</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPreparing && (
|
||||
<p className="text-xs opacity-50">Preparing AI model download...</p>
|
||||
)}
|
||||
|
||||
{error && !isLoading && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-coral-500/80">{error}</p>
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
className="text-xs text-primary-500 hover:text-primary-400 transition-colors">
|
||||
Retry download
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show Telegram login button in Tauri app, download screen on web */}
|
||||
{!isWeb && (
|
||||
<div className="mt-6">
|
||||
|
||||
Reference in New Issue
Block a user