import SwiftUI import AppKit /// A floating panel that lists every on-screen text area with grammar/spelling /// issues, shows the offending text with the errors highlighted, and offers /// one-click fixes (per issue, per field, or everything). struct LiveCheckView: View { @EnvironmentObject private var app: AppState @ObservedObject private var monitor: LiveMonitor init() { _monitor = ObservedObject(wrappedValue: AppState.shared.liveMonitor) } var body: some View { VStack(spacing: 0) { header Divider() if monitor.fields.isEmpty { emptyState } else { ScrollView { VStack(spacing: 14) { ForEach(monitor.fields) { field in FieldCard(field: field) } } .padding(16) } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .tint(GG.emerald) } private var header: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 2) { Text(summary).font(.headline) Text(monitor.activeAppName.isEmpty ? "Checking text areas on screen" : "In \(monitor.activeAppName) · \(monitor.scannedCount) text area\(monitor.scannedCount == 1 ? "" : "s") checked") .font(.caption).foregroundStyle(.secondary) } Spacer() Button { monitor.refreshSoon() } label: { Image(systemName: "arrow.clockwise") } .help("Re-scan now") if monitor.issueCount > 0 { Button { app.fixAllDetected() } label: { Label("Fix everything", systemImage: "checkmark.circle.fill") } .buttonStyle(.borderedProminent) } } .padding(16) } private var summary: String { let issues = monitor.issueCount if issues == 0 { return "No issues found" } return "\(issues) issue\(issues == 1 ? "" : "s") across \(monitor.fields.count) text area\(monitor.fields.count == 1 ? "" : "s")" } private var emptyState: some View { VStack(spacing: 10) { Image(systemName: "checkmark.seal.fill").font(.system(size: 42)).foregroundStyle(GG.emerald) Text("Looks clean").font(.title3.weight(.semibold)) Text("No grammar or spelling issues in the text on screen right now.") .font(.callout).foregroundStyle(.secondary).multilineTextAlignment(.center) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(40) } } /// One detected text area: highlighted text + its individual issues. private struct FieldCard: View { @EnvironmentObject private var app: AppState let field: DetectedField var body: some View { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 8) { Image(systemName: field.isFocused ? "cursorarrow.rays" : "text.cursor") .foregroundStyle(field.isFocused ? GG.emerald : .secondary) Text(field.label).font(.subheadline.weight(.semibold)).lineLimit(1) Text(field.roleName).font(.caption).foregroundStyle(.secondary) Spacer() Text("\(field.suggestions.count)") .font(.caption.weight(.semibold)) .padding(.horizontal, 7).padding(.vertical, 2) .background(GG.gold.opacity(0.2), in: Capsule()) .foregroundStyle(GG.gold) } Text(highlighted(field.text, field.suggestions)) .font(.callout) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) .background(Color(nsColor: .textBackgroundColor), in: RoundedRectangle(cornerRadius: 8)) VStack(spacing: 6) { ForEach(field.suggestions) { s in HStack(spacing: 8) { Circle().fill(color(for: s.kind)).frame(width: 7, height: 7) VStack(alignment: .leading, spacing: 1) { Text(s.message).font(.caption) if s.replacement != s.original && !s.replacement.isEmpty { Text("“\(s.original)” → “\(s.replacement)”") .font(.caption2.monospaced()).foregroundStyle(.secondary).lineLimit(1) } } Spacer() if s.replacement != s.original && !s.replacement.isEmpty { Button("Apply") { app.applySuggestion(s, in: field) } .controlSize(.small) } } } } HStack { Spacer() Button { app.fixDetectedField(field) } label: { Label("Fix this field", systemImage: "checkmark.circle") } .controlSize(.small) } } .padding(14) .background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 12)) .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.primary.opacity(0.07))) } private func color(for kind: GrammarKind) -> Color { switch kind { case .spelling: return .red case .grammar, .punctuation: return .orange default: return GG.gold } } /// Build an attributed string with each issue span tinted. private func highlighted(_ text: String, _ suggestions: [Suggestion]) -> AttributedString { let ns = text as NSString let sorted = suggestions.sorted { $0.location < $1.location } var result = AttributedString() var cursor = 0 for s in sorted { let loc = s.location, len = s.length guard loc >= cursor, len > 0, loc + len <= ns.length else { continue } if loc > cursor { result += AttributedString(ns.substring(with: NSRange(location: cursor, length: loc - cursor))) } var run = AttributedString(ns.substring(with: NSRange(location: loc, length: len))) let tint: Color = s.kind == .spelling ? .red : .orange run.backgroundColor = tint.opacity(0.18) run.foregroundColor = tint run.underlineStyle = .single result += run cursor = loc + len } if cursor < ns.length { result += AttributedString(ns.substring(with: NSRange(location: cursor, length: ns.length - cursor))) } return result } }