HuggingChat-Mac/Settings/ComponentsSettings.swift (156 lines of code) (raw):
//
// ComponentsSettings.swift
// HuggingChat-Mac
//
// Created by Cyril Zakka on 8/22/24.
//
import SwiftUI
import MLXLLM
import WhisperKit
struct ComponentsSettingsView: View {
@Environment(AudioModelManager.self) private var audioModelManager
@Environment(ModelManager.self) private var modelManager
@Environment(\.colorScheme) private var colorScheme
@AppStorage("localModel") private var selectedLocalModel: String = "None"
@AppStorage("selectedAudioModel") private var selectedAudioModel: String = "None"
@State private var selectedModels = Set<LocalModel.ID>()
@State private var showFilePicker = false
var body: some View {
VStack {
Table(of: LocalModel.self, selection: $selectedModels) {
TableColumn("Name") { model in
Label(model.displayName, systemImage: model.icon)
.symbolRenderingMode(.hierarchical)
}
TableColumn("Info") { model in
HStack {
switch model.downloadState {
case .notDownloaded:
if model.localURL != nil {
Text("\(model.size ?? "") on disk")
.frame(height: 20)
} else {
HStack {
Text(model.size ?? "--")
Button(action: {
switch model.modelType {
case .llm:
modelManager.downloadModel(model)
case .stt:
audioModelManager.downloadModel(model)
}
}) {
Text("GET")
.fontWeight(.medium)
.controlSize(.small)
.buttonStyle(.plain)
.frame(width: 50, height: 20)
.foregroundStyle(.blue)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(colorScheme == .dark ? .white : Color(red: 242/255, green: 242/255, blue: 247/255))
)
}
}
}
case .downloading(let progress):
HStack(spacing: 8) {
Text("\(Int(progress * 100))%")
.foregroundStyle(.secondary)
ProgressView(value: progress)
.frame(width: 60)
.progressViewStyle(.circular)
.controlSize(.mini)
.frame(width: 20, height: 20)
// Button(action: {
// modelManager.cancelDownload(for: model.id)
// }) {
// Image(systemName: "xmark.circle.fill")
// .foregroundStyle(.secondary)
// }
}
case .downloaded:
Text("\(model.size ?? "") on disk")
.frame(height: 20)
case .error(let message):
HStack {
Button(action: {}) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.secondary)
}
.help(message)
Button(action: {
switch model.modelType {
case .llm:
modelManager.downloadModel(model)
case .stt:
audioModelManager.downloadModel(model)
}
}) {
Text("GET")
.fontWeight(.medium)
.controlSize(.small)
.buttonStyle(.plain)
.frame(width: 50, height: 20)
.foregroundStyle(.blue)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(colorScheme == .dark ? .white : Color(red: 242/255, green: 242/255, blue: 247/255))
)
}
}
}
}
}
.alignment(.trailing)
} rows: {
Section("Language Models") {
ForEach(modelManager.availableModels) { localModel in
TableRow(localModel)
}
}
Section("Audio Models") {
ForEach(audioModelManager.availableLocalModels) { audioModel in
TableRow(audioModel)
}
}
}
.contextMenu(forSelectionType: LocalModel.ID.self) { items in
if !items.isEmpty {
let selectedLLMModels = items.compactMap { itemId in
modelManager.availableModels.first(where: { $0.id == itemId })
}
let selectedAudioModels = items.compactMap { itemId in
audioModelManager.availableLocalModels.first(where: { $0.id == itemId })
}
// Only show delete if any selected model is downloaded
if selectedLLMModels.contains(where: { $0.downloadState == .downloaded }) {
Button(role: .destructive) {
for model in selectedLLMModels where model.downloadState == .downloaded {
modelManager.deleteLocalModel(model)
if selectedLocalModel == model.id {
selectedLocalModel = "None"
modelManager.loadState = .idle
}
}
modelManager.fetchAllLocalModels()
} label: {
Label("Delete", systemImage: "trash")
}
}
if selectedAudioModels.contains(where: { $0.downloadState == .downloaded }) {
Button(role: .destructive) {
for model in selectedAudioModels where model.downloadState == .downloaded {
audioModelManager.deleteModel(selectedModel: model.id)
if selectedAudioModel == model.id {
selectedAudioModel = "None"
audioModelManager.modelState = .unloaded
}
}
audioModelManager.fetchModels()
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
.tableStyle(.automatic)
.alternatingRowBackgrounds(.disabled)
// HStack(alignment: .center) {
// Button(action: {
// for localModelID in selectedModels {
// if let modelToDelete = modelManager.availableModels.first(where: {$0.id == localModelID}) {
// modelManager.deleteLocalModel(modelToDelete)
// self.modelManager.fetchAllLocalModels()
// } else if let modelToDelete = audioModelManager.availableLocalModels.first(where: {$0.id == localModelID}) {
// audioModelManager.deleteModel(selectedModel: modelToDelete.id)
// self.audioModelManager.fetchModels()
// }
// }
//
// }) {
// Image(systemName: "minus").imageScale(.medium)
// }
// .disabled(selectedModels.isEmpty)
//
// Spacer()
// }
//
// .buttonStyle(.borderless)
// .frame(height: 20)
// .padding(.horizontal, 10)
}
.onAppear {
audioModelManager.fetchModels()
}
}
}
#Preview {
ComponentsSettingsView()
.environment(ModelManager())
.environment(AudioModelManager())
}