import RastrilloApple import SwiftUI struct QuickDraft: Codable { let calendar_id: Int64 let title, location, start_local, end_local, tz: String let all_day: Bool let `repeat`: String let attendees: [String] } struct QuickPreview: Decodable { let draft: QuickDraft let when, repeat_label, calendar_name: String let assumptions: [String] } struct QuickAddView: View { let client: NativeHTTP @ObservedObject var model: CalendarModel @State private var preview: QuickPreview? @State private var error = "" @State private var parsing = false @FocusState private var focused: Bool private var requestKey: String { model.zone + "\n" + model.quickText } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { TextField("Lunch tomorrow at 1pm for an hour", text: $model.quickText, axis: .vertical) .lineLimit(2...4) #if os(macOS) .textFieldStyle(.plain) .padding(10) .background(.background, in: RoundedRectangle(cornerRadius: 8)) .overlay { RoundedRectangle(cornerRadius: 8) .strokeBorder(focused ? Color.accentColor : Color.secondary.opacity(0.3), lineWidth: 1) } #else .textFieldStyle(.roundedBorder) #endif .accessibilityLabel("Describe an event").accessibilityIdentifier("quickAddText") .focused($focused).disabled(model.quickSaving) Text(model.zone.replacingOccurrences(of: "_", with: " ")).font(.footnote).foregroundStyle(.secondary) if parsing { ProgressView("Understanding your event…") } if let preview { VStack(alignment: .leading, spacing: 8) { Text(preview.draft.title).font(.headline) Label(preview.when, systemImage: "clock") Label(preview.calendar_name, systemImage: "calendar") if !preview.draft.location.isEmpty { Label(preview.draft.location, systemImage: "mappin") } if !preview.repeat_label.isEmpty { Label(preview.repeat_label, systemImage: "repeat") } if !preview.draft.attendees.isEmpty { Label(preview.draft.attendees.joined(separator: ", "), systemImage: "person.2") Text("Attendees are recorded, not invited. No invitations are sent.").font(.footnote) } ForEach(preview.assumptions, id: \.self) { Text($0).font(.footnote).foregroundStyle(.secondary) } }.accessibilityIdentifier("quickAddPreview") Button(model.quickSaving ? "Adding…" : "Add event", systemImage: "plus") { save(preview) } .buttonStyle(.borderedProminent).disabled(model.quickSaving || parsing) .accessibilityIdentifier("quickAddSave").keyboardShortcut(.return, modifiers: .command) } if !error.isEmpty { Label(error, systemImage: "exclamationmark.circle").foregroundStyle(.red) } if !model.quickSavedNotice.isEmpty { Label(model.quickSavedNotice, systemImage: "checkmark.circle").accessibilityIdentifier("quickAddSaved") } }.frame(maxWidth: .infinity, alignment: .leading) } .task(id: requestKey) { preview = nil; error = "" guard !model.quickText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { parsing = false; return } parsing = true let key = requestKey do { try await Task.sleep(for: .milliseconds(350)) let result: QuickPreview = try await client.request("/api/native/quick/parse", method: "POST", body: JSONEncoder().encode(["text": model.quickText, "zone": model.zone])) guard !Task.isCancelled, requestKey == key else { return } preview = result; parsing = false } catch { guard !Task.isCancelled, requestKey == key else { return } self.error = error.localizedDescription; parsing = false } } .onChange(of: model.quickText) { _, text in preview = nil; if !text.isEmpty { model.quickSavedNotice = "" } } .onChange(of: model.zone) { _, _ in preview = nil } } private func save(_ preview: QuickPreview) { error = "" Task { do { try await model.saveQuick(preview, client: client); self.preview = nil } catch is CancellationError { } catch { self.error = error.localizedDescription } } } }