chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
//
|
||||
// BatchFileTestViewModel.swift
|
||||
// MNNLLMiOS
|
||||
// Created by 游薪渝(揽清) on 2025/10/10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
/// ViewModel for managing batch file testing operations
|
||||
/// Handles loading test files, executing batch tests, and managing results
|
||||
@MainActor
|
||||
class BatchFileTestViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
|
||||
/// Array of test items loaded from file
|
||||
@Published var testItems: [BatchTestItem] = []
|
||||
|
||||
/// Array of test results from batch execution
|
||||
@Published var testResults: [String] = []
|
||||
|
||||
/// Whether a batch test is currently running
|
||||
@Published var isTesting: Bool = false
|
||||
|
||||
/// Selected output file format
|
||||
@Published var selectedFormat: BatchTestFileFormat = .jsonl
|
||||
|
||||
/// URL for sharing the results file
|
||||
@Published var shareFileURL: URL?
|
||||
|
||||
/// Whether to show the share sheet
|
||||
@Published var showingShareSheet: Bool = false
|
||||
|
||||
/// Error message to display
|
||||
@Published var errorMessage: String = ""
|
||||
|
||||
/// Whether to show error alert
|
||||
@Published var showingError: Bool = false
|
||||
|
||||
/// Current batch test type
|
||||
@Published var currentTestType: BatchTestType = .text
|
||||
|
||||
// MARK: - Private Properties
|
||||
|
||||
/// Reference to the chat view model for LLM operations
|
||||
private let chatViewModel: LLMChatViewModel
|
||||
|
||||
/// Service for batch file operations
|
||||
private let batchService = BatchFileTestService()
|
||||
|
||||
/// Flag to track pending share sheet presentation
|
||||
private var pendingSharePresentation = false
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Initializes the view model with a chat view model reference
|
||||
/// - Parameter chatViewModel: The LLM chat view model for processing requests
|
||||
init(chatViewModel: LLMChatViewModel) {
|
||||
self.chatViewModel = chatViewModel
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Starts the batch testing process
|
||||
/// Executes all test items and collects results
|
||||
func startBatchTest() {
|
||||
guard !testItems.isEmpty else {
|
||||
showErrorMessage("No test items available. Please load a test file first.")
|
||||
return
|
||||
}
|
||||
|
||||
guard !isTesting else {
|
||||
showErrorMessage("Batch test is already in progress.")
|
||||
return
|
||||
}
|
||||
|
||||
isTesting = true
|
||||
testResults.removeAll()
|
||||
|
||||
performBatchTest()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Loads test file from the specified URL with proper permission handling
|
||||
/// - Parameter url: URL of the test file to load
|
||||
func loadTestFile(from url: URL) {
|
||||
// For files selected through DocumentPicker, we always need security-scoped access
|
||||
// This is required for accessing files outside the app's sandbox
|
||||
let hasAccess = url.startAccessingSecurityScopedResource()
|
||||
|
||||
// Ensure we stop accessing the resource when done
|
||||
defer {
|
||||
if hasAccess {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let content = try String(contentsOf: url)
|
||||
testItems = try parseFileContent(content, fileExtension: url.pathExtension.lowercased())
|
||||
|
||||
if testItems.isEmpty {
|
||||
showErrorMessage("No valid test items found in the selected file.")
|
||||
}
|
||||
} catch {
|
||||
showErrorMessage("Failed to load test file: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads test files from LocalBatchTest folder based on test type
|
||||
/// - Parameter testType: The type of test to load
|
||||
func loadLocalBatchTest(for testType: BatchTestType) {
|
||||
currentTestType = testType
|
||||
|
||||
guard let bundlePath = Bundle.main.path(forResource: "LocalBatchTest", ofType: nil),
|
||||
let localBatchTestURL = URL(string: "file://\(bundlePath)") else {
|
||||
showErrorMessage("LocalBatchTest folder not found in bundle.")
|
||||
return
|
||||
}
|
||||
|
||||
let testFolderURL = localBatchTestURL.appendingPathComponent(testType.localBatchTestFolder)
|
||||
let promptFileURL = testFolderURL.appendingPathComponent("prompt.jsonl")
|
||||
|
||||
do {
|
||||
let content = try String(contentsOf: promptFileURL)
|
||||
testItems = try parseJSONLContent(content, testType: testType, baseURL: testFolderURL)
|
||||
|
||||
if testItems.isEmpty {
|
||||
showErrorMessage("No valid test items found in \(testType.displayName) file.")
|
||||
}
|
||||
} catch {
|
||||
showErrorMessage("Failed to load \(testType.displayName) file: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates and shares the results file
|
||||
/// - Parameter scenePhase: Current scene phase to control presentation timing
|
||||
func generateResultsFile(scenePhase: ScenePhase) {
|
||||
guard !testResults.isEmpty else {
|
||||
showErrorMessage("No test results available to share.")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let fileURL = try generateOutputFile(results: testResults, format: selectedFormat)
|
||||
shareFileURL = fileURL
|
||||
presentShareSheetIfActive(scenePhase: scenePhase)
|
||||
} catch {
|
||||
showErrorMessage("Failed to generate results file: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Presents the share sheet if the app is in active state
|
||||
/// - Parameter scenePhase: Current scene phase
|
||||
private func presentShareSheetIfActive(scenePhase: ScenePhase) {
|
||||
if scenePhase == .active {
|
||||
showingShareSheet = true
|
||||
pendingSharePresentation = false
|
||||
} else {
|
||||
pendingSharePresentation = true
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles scene phase changes for share sheet presentation
|
||||
/// - Parameter scenePhase: New scene phase
|
||||
func handleScenePhaseChange(_ scenePhase: ScenePhase) {
|
||||
if scenePhase == .active && pendingSharePresentation {
|
||||
showingShareSheet = true
|
||||
pendingSharePresentation = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the share sheet presentation state
|
||||
func onShareSheetDismiss() {
|
||||
pendingSharePresentation = false
|
||||
}
|
||||
|
||||
/// Resets all test data and state
|
||||
func reset() {
|
||||
isTesting = false
|
||||
testItems.removeAll()
|
||||
testResults.removeAll()
|
||||
shareFileURL = nil
|
||||
showingShareSheet = false
|
||||
pendingSharePresentation = false
|
||||
errorMessage = ""
|
||||
showingError = false
|
||||
currentTestType = .text
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/// Parses file content based on extension
|
||||
/// - Parameters:
|
||||
/// - content: File content string
|
||||
/// - fileExtension: File extension
|
||||
/// - Returns: Array of parsed test items
|
||||
private func parseFileContent(_ content: String, fileExtension: String) throws -> [BatchTestItem] {
|
||||
switch fileExtension {
|
||||
case "jsonl":
|
||||
return try parseJSONLContent(content, testType: .text, baseURL: nil)
|
||||
case "txt":
|
||||
return parseTextContent(content)
|
||||
default:
|
||||
throw NSError(domain: "BatchFileTestViewModel", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unsupported file format: \(fileExtension)"])
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses JSONL content with support for images and audio
|
||||
/// - Parameters:
|
||||
/// - content: JSONL content string
|
||||
/// - testType: Type of test being parsed
|
||||
/// - baseURL: Base URL for resolving relative paths
|
||||
/// - Returns: Array of parsed test items
|
||||
private func parseJSONLContent(_ content: String, testType: BatchTestType, baseURL: URL?) throws -> [BatchTestItem] {
|
||||
let lines = content.components(separatedBy: .newlines)
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
var items: [BatchTestItem] = []
|
||||
|
||||
for line in lines {
|
||||
guard let data = line.data(using: .utf8),
|
||||
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let prompt = json["prompt"] as? String else {
|
||||
continue
|
||||
}
|
||||
|
||||
let processedPrompt = batchService.processMediaReferences(json: json, prompt: prompt, baseURL: baseURL)
|
||||
|
||||
let item = BatchTestItem(
|
||||
prompt: processedPrompt,
|
||||
testType: testType
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/// Parses plain text content
|
||||
/// - Parameter content: Text content string
|
||||
/// - Returns: Array of parsed test items
|
||||
private func parseTextContent(_ content: String) -> [BatchTestItem] {
|
||||
return content.components(separatedBy: .newlines)
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.map { BatchTestItem(prompt: $0, testType: .text) }
|
||||
}
|
||||
|
||||
/// Performs the actual batch testing operation
|
||||
private func performBatchTest() {
|
||||
let totalItems = testItems.count
|
||||
guard totalItems > 0 else { return }
|
||||
|
||||
// Extract prompts from test items
|
||||
let prompts = testItems.map { $0.prompt }
|
||||
|
||||
chatViewModel.getBatchLLMResponse(prompts: prompts) { [weak self] (responses: [String]) in
|
||||
Task { @MainActor in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Update results
|
||||
self.testResults = responses
|
||||
self.isTesting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates output file with test results
|
||||
/// - Parameters:
|
||||
/// - results: Array of test result strings
|
||||
/// - format: Output file format
|
||||
/// - Returns: URL of the generated file
|
||||
private func generateOutputFile(results: [String], format: BatchTestFileFormat) throws -> URL {
|
||||
// Convert simple string results to structured BatchTestResult array
|
||||
let structuredResults: [BatchTestResult] = results.enumerated().map { index, output in
|
||||
let prompt = index < testItems.count ? testItems[index].prompt : ""
|
||||
let item = index < testItems.count ? testItems[index] : BatchTestItem(prompt: prompt, testType: .text)
|
||||
return BatchTestResult(item: item, response: output, timestamp: Date())
|
||||
}
|
||||
|
||||
// Use default configuration and generate output
|
||||
let configuration = BatchTestConfiguration(
|
||||
outputFormat: format,
|
||||
includeTimestamp: true,
|
||||
includePrompt: true
|
||||
)
|
||||
|
||||
return try batchService.generateOutputFile(
|
||||
results: structuredResults,
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
|
||||
/// Shows an error message to the user
|
||||
/// - Parameter message: Error message to display
|
||||
private func showErrorMessage(_ message: String) {
|
||||
errorMessage = message
|
||||
showingError = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user