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:
2026-04-06 14:26:08 +02:00
commit bbcf0c74bb
28 changed files with 5747 additions and 0 deletions
@@ -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)")
}
}