| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  | // | 
					
						
							|  |  |  | //  ModelSelectionCard.swift | 
					
						
							|  |  |  | //  MNNLLMiOS | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | //  Created by 游薪渝(揽清) on 2025/7/21. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import SwiftUI | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /**
 | 
					
						
							|  |  |  |  * Reusable model selection card component for benchmark interface. | 
					
						
							|  |  |  |  * Provides dropdown menu for model selection and start/stop controls. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | struct ModelSelectionCard: View { | 
					
						
							|  |  |  |     @ObservedObject var viewModel: BenchmarkViewModel | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     var body: some View { | 
					
						
							|  |  |  |         VStack(alignment: .leading, spacing: 16) { | 
					
						
							|  |  |  |             HStack { | 
					
						
							|  |  |  |                 Text("Select Model") | 
					
						
							|  |  |  |                     .font(.title3) | 
					
						
							|  |  |  |                     .fontWeight(.semibold) | 
					
						
							|  |  |  |                     .foregroundColor(.primary) | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 Spacer() | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             if viewModel.isLoading { | 
					
						
							|  |  |  |                 HStack { | 
					
						
							|  |  |  |                     ProgressView() | 
					
						
							|  |  |  |                         .scaleEffect(0.8) | 
					
						
							|  |  |  |                     Text("Loading models...") | 
					
						
							|  |  |  |                         .font(.subheadline) | 
					
						
							|  |  |  |                         .foregroundColor(.secondary) | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 .frame(maxWidth: .infinity, alignment: .leading) | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |                 modelDropdownMenu | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             startStopButton | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             statusMessages | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         .padding(20) | 
					
						
							|  |  |  |         .background( | 
					
						
							|  |  |  |             RoundedRectangle(cornerRadius: 16) | 
					
						
							|  |  |  |                 .fill(Color.benchmarkCardBg) | 
					
						
							|  |  |  |                 .overlay( | 
					
						
							|  |  |  |                     RoundedRectangle(cornerRadius: 16) | 
					
						
							|  |  |  |                         .stroke(Color.benchmarkSuccess.opacity(0.3), lineWidth: 1) | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Private Views | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private var modelDropdownMenu: some View { | 
					
						
							|  |  |  |         Menu { | 
					
						
							|  |  |  |             if viewModel.availableModels.isEmpty { | 
					
						
							|  |  |  |                 Button("No models available") { | 
					
						
							|  |  |  |                     // Placeholder - no action | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 .disabled(true) | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |                 ForEach(viewModel.availableModels, id: \.id) { model in | 
					
						
							|  |  |  |                     Button(action: { | 
					
						
							|  |  |  |                         viewModel.onModelSelected(model) | 
					
						
							|  |  |  |                     }) { | 
					
						
							|  |  |  |                         HStack { | 
					
						
							|  |  |  |                             VStack(alignment: .leading, spacing: 2) { | 
					
						
							|  |  |  |                                 Text(model.modelName) | 
					
						
							|  |  |  |                                     .font(.system(size: 14, weight: .medium)) | 
					
						
							|  |  |  |                                 Text("Local") | 
					
						
							|  |  |  |                                     .font(.caption) | 
					
						
							|  |  |  |                                     .foregroundColor(.secondary) | 
					
						
							|  |  |  |                             } | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } label: { | 
					
						
							|  |  |  |             HStack(spacing: 16) { | 
					
						
							|  |  |  |                 VStack(alignment: .leading, spacing: 6) { | 
					
						
							| 
									
										
										
										
											2025-07-21 17:06:00 +08:00
										 |  |  |                     Text(viewModel.selectedModel?.modelName ?? String(localized: "Choose your AI model")) | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                         .font(.system(size: 16, weight: .medium)) | 
					
						
							| 
									
										
										
										
											2025-07-22 10:50:47 +08:00
										 |  |  |                         .foregroundColor(viewModel.isRunning ? .secondary : (viewModel.selectedModel != nil ? .primary : .benchmarkSecondary)) | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                         .lineLimit(1) | 
					
						
							|  |  |  |                      | 
					
						
							|  |  |  |                     if let model = viewModel.selectedModel { | 
					
						
							|  |  |  |                         HStack(spacing: 8) { | 
					
						
							|  |  |  |                             HStack(spacing: 4) { | 
					
						
							|  |  |  |                                 Circle() | 
					
						
							|  |  |  |                                     .fill(Color.benchmarkSuccess) | 
					
						
							|  |  |  |                                     .frame(width: 6, height: 6) | 
					
						
							|  |  |  |                                 Text("Ready") | 
					
						
							|  |  |  |                                     .font(.caption) | 
					
						
							|  |  |  |                                     .foregroundColor(.benchmarkSuccess) | 
					
						
							|  |  |  |                             } | 
					
						
							|  |  |  |                              | 
					
						
							|  |  |  |                             if let size = model.cachedSize { | 
					
						
							|  |  |  |                                 Text("• \(formatBytes(size))") | 
					
						
							|  |  |  |                                     .font(.caption) | 
					
						
							|  |  |  |                                     .foregroundColor(.benchmarkSecondary) | 
					
						
							|  |  |  |                             } | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                     } else { | 
					
						
							|  |  |  |                         Text("Tap to select a model for testing") | 
					
						
							|  |  |  |                             .font(.caption) | 
					
						
							|  |  |  |                             .foregroundColor(.benchmarkSecondary) | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 Spacer() | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 Image(systemName: "chevron.down") | 
					
						
							|  |  |  |                     .font(.system(size: 14, weight: .medium)) | 
					
						
							| 
									
										
										
										
											2025-07-22 10:50:47 +08:00
										 |  |  |                     .foregroundColor(viewModel.isRunning ? .secondary : .benchmarkSecondary) | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                     .rotationEffect(.degrees(0)) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             .padding(20) | 
					
						
							|  |  |  |             .background( | 
					
						
							|  |  |  |                 RoundedRectangle(cornerRadius: 16) | 
					
						
							|  |  |  |                     .fill(Color.benchmarkCardBg) | 
					
						
							|  |  |  |                     .overlay( | 
					
						
							|  |  |  |                         RoundedRectangle(cornerRadius: 16) | 
					
						
							|  |  |  |                             .stroke( | 
					
						
							| 
									
										
										
										
											2025-07-22 10:50:47 +08:00
										 |  |  |                                 viewModel.isRunning ?  | 
					
						
							|  |  |  |                                 Color.gray.opacity(0.1) : | 
					
						
							|  |  |  |                                 (viewModel.selectedModel != nil ?  | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                                 Color.benchmarkAccent.opacity(0.3) :  | 
					
						
							| 
									
										
										
										
											2025-07-22 10:50:47 +08:00
										 |  |  |                                 Color.gray.opacity(0.2)), | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                                 lineWidth: 1 | 
					
						
							|  |  |  |                             ) | 
					
						
							| 
									
										
										
										
											2025-07-22 10:50:47 +08:00
										 |  |  |                     )) | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-07-22 10:50:47 +08:00
										 |  |  |         .disabled(viewModel.isRunning) | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private var startStopButton: some View { | 
					
						
							|  |  |  |         Button(action: { | 
					
						
							| 
									
										
										
										
											2025-08-05 15:43:12 +08:00
										 |  |  |             viewModel.onStartBenchmarkTapped() | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |         }) { | 
					
						
							|  |  |  |             HStack(spacing: 12) { | 
					
						
							|  |  |  |                 ZStack { | 
					
						
							|  |  |  |                     Circle() | 
					
						
							|  |  |  |                         .fill(Color.white.opacity(0.2)) | 
					
						
							|  |  |  |                         .frame(width: 32, height: 32) | 
					
						
							|  |  |  |                      | 
					
						
							| 
									
										
										
										
											2025-08-05 15:43:12 +08:00
										 |  |  |                     if viewModel.isRunning { | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                         ProgressView() | 
					
						
							|  |  |  |                             .progressViewStyle(CircularProgressViewStyle(tint: .white)) | 
					
						
							|  |  |  |                             .scaleEffect(0.7) | 
					
						
							|  |  |  |                     } else { | 
					
						
							| 
									
										
										
										
											2025-08-05 15:43:12 +08:00
										 |  |  |                         Image(systemName: viewModel.isRunning ? "stop.fill" : "play.fill") | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                             .font(.system(size: 16, weight: .bold)) | 
					
						
							|  |  |  |                             .foregroundColor(.white) | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 Text(viewModel.startButtonText) | 
					
						
							|  |  |  |                     .font(.system(size: 18, weight: .semibold)) | 
					
						
							|  |  |  |                     .foregroundColor(.white) | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 Spacer() | 
					
						
							|  |  |  |                  | 
					
						
							| 
									
										
										
										
											2025-08-05 15:43:12 +08:00
										 |  |  |                 if !viewModel.isRunning { | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                     Image(systemName: "arrow.right") | 
					
						
							|  |  |  |                         .font(.system(size: 16, weight: .semibold)) | 
					
						
							|  |  |  |                         .foregroundColor(.white.opacity(0.8)) | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             .frame(maxWidth: .infinity) | 
					
						
							|  |  |  |             .padding(.horizontal, 24) | 
					
						
							|  |  |  |             .padding(.vertical, 18) | 
					
						
							|  |  |  |             .background( | 
					
						
							|  |  |  |                 RoundedRectangle(cornerRadius: 16) | 
					
						
							|  |  |  |                     .fill( | 
					
						
							|  |  |  |                         viewModel.isStartButtonEnabled ?  | 
					
						
							| 
									
										
										
										
											2025-08-06 10:49:02 +08:00
										 |  |  |                         (viewModel.isRunning ?  | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |                          LinearGradient( | 
					
						
							|  |  |  |                              colors: [Color.benchmarkError, Color.benchmarkError.opacity(0.8)], | 
					
						
							|  |  |  |                              startPoint: .leading, | 
					
						
							|  |  |  |                              endPoint: .trailing | 
					
						
							|  |  |  |                          ) : | 
					
						
							|  |  |  |                          LinearGradient( | 
					
						
							|  |  |  |                              colors: [Color.benchmarkGradientStart, Color.benchmarkGradientEnd], | 
					
						
							|  |  |  |                              startPoint: .leading, | 
					
						
							|  |  |  |                              endPoint: .trailing | 
					
						
							|  |  |  |                          )) : | 
					
						
							|  |  |  |                         LinearGradient( | 
					
						
							|  |  |  |                             colors: [Color.gray, Color.gray.opacity(0.8)], | 
					
						
							|  |  |  |                             startPoint: .leading, | 
					
						
							|  |  |  |                             endPoint: .trailing | 
					
						
							|  |  |  |                         ) | 
					
						
							|  |  |  |                     ) | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         .disabled(!viewModel.isStartButtonEnabled || viewModel.selectedModel == nil) | 
					
						
							|  |  |  |         .animation(.easeInOut(duration: 0.2), value: viewModel.startButtonText) | 
					
						
							|  |  |  |         .animation(.easeInOut(duration: 0.2), value: viewModel.isStartButtonEnabled) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private var statusMessages: some View { | 
					
						
							|  |  |  |         Group { | 
					
						
							|  |  |  |             if viewModel.selectedModel == nil { | 
					
						
							|  |  |  |                 Text("Start benchmark after selecting your model") | 
					
						
							|  |  |  |                     .font(.caption) | 
					
						
							|  |  |  |                     .foregroundColor(.orange) | 
					
						
							|  |  |  |                     .padding(.horizontal, 16) | 
					
						
							|  |  |  |             } else if viewModel.availableModels.isEmpty { | 
					
						
							|  |  |  |                 Text("No local models found. Please download a model first.") | 
					
						
							|  |  |  |                     .font(.caption) | 
					
						
							|  |  |  |                     .foregroundColor(.orange) | 
					
						
							|  |  |  |                     .padding(.horizontal, 16) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // MARK: - Helper Functions | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     private func formatBytes(_ bytes: Int64) -> String { | 
					
						
							|  |  |  |         let formatter = ByteCountFormatter() | 
					
						
							|  |  |  |         formatter.allowedUnits = [.useGB, .useMB] | 
					
						
							|  |  |  |         formatter.countStyle = .file | 
					
						
							|  |  |  |         return formatter.string(fromByteCount: bytes) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #Preview { | 
					
						
							|  |  |  |     ModelSelectionCard( | 
					
						
							| 
									
										
										
										
											2025-08-05 15:43:12 +08:00
										 |  |  |         viewModel: BenchmarkViewModel() | 
					
						
							| 
									
										
										
										
											2025-07-21 14:19:44 +08:00
										 |  |  |     ) | 
					
						
							|  |  |  |     .padding() | 
					
						
							| 
									
										
										
										
											2025-07-22 10:50:47 +08:00
										 |  |  | } |