Files
2026-07-13 13:33:03 +08:00

306 lines
9.7 KiB
Swift

//
// BatchFileTestView.swift
// MNNLLMiOS
//
// Created by 游薪渝 on 2025/9/29.
//
import Foundation
import SwiftUI
struct BatchFileTestView: View {
// MARK: - Properties
/// ViewModel for managing batch test operations
@StateObject private var viewModel: BatchFileTestViewModel
/// Current scene phase for managing share sheet presentation
@Environment(\.scenePhase) private var scenePhase
/// Presentation state for document picker
@State private var showingDocumentPicker = false
/// Presentation state for test type selection
@State private var showingTestTypeSelection = false
// MARK: - Initialization
/// Initializes the view with a chat view model
/// - Parameter chatViewModel: The LLM chat view model for processing requests
init(chatViewModel: LLMChatViewModel) {
_viewModel = StateObject(wrappedValue: BatchFileTestViewModel(chatViewModel: chatViewModel))
}
// MARK: - Body
var body: some View {
NavigationView {
VStack(spacing: 20) {
// File Loading Section
fileLoadingSection
// Test Items Display
testItemsSection
// Test Controls
testControlsSection
// Results Section
resultsSection
Spacer()
}
.padding()
.navigationTitle("Batch File Test")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showingDocumentPicker) {
DocumentPicker { result in
handleFileSelection(result)
}
}
.sheet(isPresented: $viewModel.showingShareSheet, onDismiss: viewModel.onShareSheetDismiss) {
if let shareURL = viewModel.shareFileURL {
ShareSheet(activityItems: [shareURL], onDismiss: viewModel.onShareSheetDismiss)
}
}
.alert("Error", isPresented: $viewModel.showingError) {
Button("OK") {}
} message: {
Text(viewModel.errorMessage)
}
.actionSheet(isPresented: $showingTestTypeSelection) {
ActionSheet(
title: Text("Select Test Type"),
message: Text("Choose the type of batch test to run"),
buttons: [
.default(Text("Text Test")) {
viewModel.loadLocalBatchTest(for: .text)
},
.default(Text("Image Test")) {
viewModel.loadLocalBatchTest(for: .image)
},
.default(Text("Audio Test")) {
viewModel.loadLocalBatchTest(for: .audio)
},
.cancel(),
]
)
}
}
.onChange(of: scenePhase) { newPhase in
viewModel.handleScenePhaseChange(newPhase)
}
}
// MARK: - View Components
/// File loading section with options for local and external files
private var fileLoadingSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Load Test Files")
.font(.headline)
HStack {
Button("Load Local Tests") {
showingTestTypeSelection = true
}
.buttonStyle(.bordered)
Spacer()
Button("Load External File") {
showingDocumentPicker = true
}
.buttonStyle(.bordered)
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
.frame(maxWidth: .infinity)
}
/// Test items display section
private var testItemsSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Test Items")
.font(.headline)
Spacer()
Text("\(viewModel.testItems.count) items")
.foregroundColor(.secondary)
}
if !viewModel.testItems.isEmpty {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(Array(viewModel.testItems.enumerated()), id: \.offset) { index, item in
testItemRow(item: item, index: index)
}
}
}
.frame(maxHeight: 400)
} else {
Text("No test items loaded")
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .center)
.padding()
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
}
/// Individual test item row
private func testItemRow(item: BatchTestItem, index: Int) -> some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Item \(index + 1)")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
// Test type indicator
Text(item.testType.displayName)
.font(.caption)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(testTypeColor(item.testType))
.foregroundColor(.white)
.cornerRadius(4)
}
Text(item.prompt)
.font(.caption)
.lineLimit(2)
.foregroundColor(.primary)
}
.padding(8)
.background(Color(.systemBackground))
.cornerRadius(6)
}
/// Returns color for test type indicator
private func testTypeColor(_ testType: BatchTestType) -> Color {
switch testType {
case .text:
return .blue
case .image:
return .orange
case .audio:
return .green
}
}
/// Test controls section
private var testControlsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Test Controls")
.font(.headline)
if viewModel.isTesting {
VStack(spacing: 8) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
Text("Testing, please wait...")
.font(.caption)
.foregroundColor(.secondary)
}
} else {
HStack(spacing: 12) {
Button("Start Batch Test") {
viewModel.startBatchTest()
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.testItems.isEmpty)
Spacer()
Button("Reset") {
viewModel.reset()
}
.buttonStyle(.bordered)
}
}
}
.padding()
.frame(maxWidth: .infinity)
.background(Color(.systemGray6))
.cornerRadius(10)
}
/// Results section
private var resultsSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Results")
.font(.headline)
Spacer()
if !viewModel.testResults.isEmpty {
Text("\(viewModel.testResults.count) results")
.foregroundColor(.secondary)
}
}
if !viewModel.testResults.isEmpty {
VStack(spacing: 8) {
// Format selection
HStack {
Text("Export Format:")
.font(.caption)
Spacer()
}
Picker("Format", selection: $viewModel.selectedFormat) {
ForEach(BatchTestFileFormat.allCases, id: \.self) { format in
Text(format.displayName).tag(format)
}
}
.pickerStyle(SegmentedPickerStyle())
HStack {
Button("Share Results") {
viewModel.generateResultsFile(scenePhase: scenePhase)
}
.buttonStyle(.borderedProminent)
Spacer()
}
}
} else {
Text("No results available")
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .center)
.padding()
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
}
// MARK: - Helper Methods
/// Handles file selection from document picker
/// - Parameter result: Result from document picker
private func handleFileSelection(_ result: Result<[URL], Error>) {
switch result {
case let .success(urls):
guard let selectedURL = urls.first else { return }
// Pass the URL directly to ViewModel for proper permission handling
viewModel.loadTestFile(from: selectedURL)
case let .failure(error):
viewModel.errorMessage = "File selection failed: \(error.localizedDescription)"
viewModel.showingError = true
}
}
}