Initial iOS port - Complete source code and build system
- 19 Swift source files (~4900 lines) - Complete UI with SwiftUI (MainView, SettingsView, MessageBubble, InputBar) - Inference layer (LlmEngine, Agent, ToolCalling, ConversationContext) - Services (Audio, TTS, WebSearch, ModelDownload, Storage) - Build system: Makefile, Package.swift, Podfile - Documentation: BUILD.md, plan.md, PROJECT_STATUS.md - Ready for Xcode build - just need LiteRT dependency added
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user