import Foundation import SwiftUI @MainActor class SettingsViewModel: ObservableObject { @Published var selectedModel: ModelVariant = .e2b @Published var modelPath: String = "" @Published var isModelLoaded: Bool = false @Published var isModelLoading: Bool = false @Published var searchServerUrl: String = "" @Published var delegateServerUrl: String = "" @Published var ttsEnabled: Bool = true @Published var ttsAutoMode: Bool = true @Published var downloadProgress: Double = 0 @Published var isDownloading: Bool = false @Published var downloadStatus: String = "" @Published var deviceInfo: DeviceInfo = DeviceInfo() @Published var errorMessage: String? @Published var showError: Bool = false private let modelDownloadService: ModelDownloadService private let llmEngine: LlmEngine private let userDefaults = UserDefaults.standard init(modelDownloadService: ModelDownloadService, llmEngine: LlmEngine) { self.modelDownloadService = modelDownloadService self.llmEngine = llmEngine loadSettings() updateDeviceInfo() } func loadModel() async { guard !modelPath.isEmpty else { showError("No model selected") return } isModelLoading = true defer { isModelLoading = false } do { try await llmEngine.loadModel(path: modelPath) isModelLoaded = true saveSettings() } catch { showError("Failed to load model: \(error.localizedDescription)") isModelLoaded = false } } func downloadModel(variant: ModelVariant) async { guard !isDownloading else { return } isDownloading = true downloadProgress = 0 let (url, filename): (String, String) switch variant { case .e2b: url = "https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm" filename = "gemma-4-E2B-it.litertlm" case .e4b: url = "https://huggingface.co/litert-community/gemma-4-E4B-it-litert-lm/resolve/main/gemma-4-E4B-it.litertlm" filename = "gemma-4-E4B-it.litertlm" case .custom: showError("Cannot download custom model") isDownloading = false return } do { let stream = modelDownloadService.downloadModel(from: url, filename: filename) for try await progress in stream { downloadProgress = progress downloadStatus = String(format: "%.1f%%", progress * 100) } selectedModel = variant modelPath = modelDownloadService.localPath(for: filename) await loadModel() } catch { showError("Download failed: \(error.localizedDescription)") } isDownloading = false } func deleteModel(variant: ModelVariant) { let filename: String switch variant { case .e2b: filename = "gemma-4-E2B-it.litertlm" case .e4b: filename = "gemma-4-E4B-it.litertlm" case .custom: return } modelDownloadService.deleteModel(filename: filename) if selectedModel == variant { selectedModel = .e2b modelPath = "" isModelLoaded = false } } func isModelDownloaded(variant: ModelVariant) -> Bool { let filename: String switch variant { case .e2b: filename = "gemma-4-E2B-it.litertlm" case .e4b: filename = "gemma-4-E4B-it.litertlm" case .custom: return !modelPath.isEmpty } return modelDownloadService.isDownloaded(filename: filename) } func saveSettings() { userDefaults.set(selectedModel.rawValue, forKey: "selectedModel") userDefaults.set(modelPath, forKey: "modelPath") userDefaults.set(searchServerUrl, forKey: "searchServerUrl") userDefaults.set(delegateServerUrl, forKey: "delegateServerUrl") userDefaults.set(ttsEnabled, forKey: "ttsEnabled") userDefaults.set(ttsAutoMode, forKey: "ttsAutoMode") } func updateDeviceInfo() { deviceInfo = DeviceInfo.current() } private func loadSettings() { if let modelRaw = userDefaults.string(forKey: "selectedModel"), let model = ModelVariant(rawValue: modelRaw) { selectedModel = model } modelPath = userDefaults.string(forKey: "modelPath") ?? "" searchServerUrl = userDefaults.string(forKey: "searchServerUrl") ?? "" delegateServerUrl = userDefaults.string(forKey: "delegateServerUrl") ?? "" ttsEnabled = userDefaults.object(forKey: "ttsEnabled") as? Bool ?? true ttsAutoMode = userDefaults.object(forKey: "ttsAutoMode") as? Bool ?? true isModelLoaded = llmEngine.isLoaded } private func showError(_ message: String) { errorMessage = message showError = true } } struct DeviceInfo { let totalRAM: String let freeRAM: String let deviceModel: String let systemVersion: String static func current() -> DeviceInfo { let device = UIDevice.current let physicalMemory = ProcessInfo.processInfo.physicalMemory let totalRAM = String(format: "%.0f GB", Double(physicalMemory) / 1024 / 1024 / 1024) return DeviceInfo(totalRAM: totalRAM, freeRAM: "Unknown", deviceModel: device.model, systemVersion: "\(device.systemName) \(device.systemVersion)") } }