import AppKit import Combine import Foundation @MainActor final class GrammarCheckerViewModel: ObservableObject { static let maxWords = 1_000 @Published var text: String = "" @Published private(set) var issues: [GrammarIssue] = [] @Published private(set) var correctedText: String = "" @Published private(set) var hasResults = false @Published private(set) var isChecking = false @Published private(set) var errorMessage: String? private let grammarCheckService: any GrammarCheckServicing private let historyStorage: HistoryStoring private let textExtractionService: any TextExtracting init( grammarCheckService: any GrammarCheckServicing = GrammarCheckService(), historyStorage: HistoryStoring = HistoryStorageService.shared, textExtractionService: any TextExtracting = TextExtractionService() ) { self.grammarCheckService = grammarCheckService self.historyStorage = historyStorage self.textExtractionService = textExtractionService } var wordCountLimitLabel: String { "\(text.wordCount)/\(Self.maxWords)" } var issueCountLabel: String { let count = issues.count return "\(count) Issue\(count == 1 ? "" : "s")" } var canCheck: Bool { !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isChecking } func pasteText() { guard let clipboard = NSPasteboard.general.string(forType: .string) else { return } text = String.importedApplyingWordLimit(clipboard, maxWords: Self.maxWords, setError: { errorMessage = $0 }) resetResults() } func uploadFile() { Task { do { guard let extracted = try await textExtractionService.pickAndExtractText() else { return } text = String.importedApplyingWordLimit(extracted, maxWords: Self.maxWords, setError: { errorMessage = $0 }) resetResults() } catch { errorMessage = error.localizedDescription } } } func notifyWordLimitReached() { errorMessage = String.wordLimitExceededMessage(maxWords: Self.maxWords) } func handleTextChange() { let (limited, exceeded) = text.applyingWordLimit(maxWords: Self.maxWords) if exceeded { text = limited errorMessage = String.wordLimitExceededMessage(maxWords: Self.maxWords) } else if errorMessage == String.wordLimitExceededMessage(maxWords: Self.maxWords) { errorMessage = nil } if hasResults { resetResults() } } func clearText() { text = "" resetResults() } func resetResults() { issues = [] correctedText = "" hasResults = false errorMessage = nil } func checkGrammar() { let content = text.trimmingCharacters(in: .whitespacesAndNewlines) guard !content.isEmpty else { resetResults() return } isChecking = true errorMessage = nil Task { do { let result = try await grammarCheckService.checkGrammar(in: content) issues = result.issues correctedText = result.correctedText hasResults = true saveToHistory(text: content, issues: result.issues, correctedText: result.correctedText) } catch { errorMessage = error.localizedDescription } isChecking = false } } func copyCorrectedText() { guard !correctedText.isEmpty else { return } NSPasteboard.general.clearContents() NSPasteboard.general.setString(correctedText, forType: .string) } func restore(from entry: HistoryEntry) { guard case .grammarChecker(let data) = entry.payload else { return } text = data.text issues = data.issues.map { GrammarIssue(original: $0.original, suggestion: $0.suggestion, explanation: $0.explanation) } correctedText = data.correctedText hasResults = true errorMessage = nil } private func saveToHistory(text: String, issues: [GrammarIssue], correctedText: String) { let payload = HistoryPayload.grammarChecker( GrammarCheckerHistoryData( text: text, issues: issues.map(\.stored), correctedText: correctedText ) ) let entry = HistoryEntry( tool: .grammarChecker, title: HistoryHelpers.title(from: text, fallback: "Grammar Check"), preview: HistoryHelpers.preview(from: correctedText.isEmpty ? text : correctedText), payload: payload ) historyStorage.save(entry) } }