// // CandidatesView.swift // iris // // Created by Codex. // import SwiftUI struct CandidatesView: View { @StateObject private var model = CandidatesViewModel() var body: some View { NavigationStack { List { Section("Source") { LabeledContent("Location") { Text("London (demo)") } if let updated = model.lastUpdatedAt { LabeledContent("Last update") { Text(timeOnly(from: updated)) } } else { LabeledContent("Last update") { Text("Never") } } if let error = model.lastError { Text(error) .font(.footnote) .foregroundStyle(.secondary) } } if !model.diagnostics.isEmpty { Section("Diagnostics") { ForEach(model.diagnostics.keys.sorted(), id: \.self) { key in LabeledContent(key) { Text(model.diagnostics[key] ?? "") .font(.caption) .foregroundStyle(.secondary) .textSelection(.enabled) } } } } Section("Candidates (\(model.candidates.count))") { if model.candidates.isEmpty { Text(model.isLoading ? "Loading…" : "No candidates") .foregroundStyle(.secondary) } else { ForEach(model.candidates, id: \.id) { candidate in CandidateRow(candidate: candidate) } } } } .navigationTitle("Candidates") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(model.isLoading ? "Refreshing…" : "Refresh") { model.refresh() } .disabled(model.isLoading) } } .onAppear { model.refresh() } } } private func timeOnly(from date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .none formatter.timeStyle = .medium return formatter.string(from: date) } } private struct CandidateRow: View { let candidate: Candidate var body: some View { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .firstTextBaseline) { Text(candidate.title) .font(.headline) .lineLimit(1) Spacer() Text(candidate.type.rawValue) .font(.caption) .foregroundStyle(.secondary) } Text(candidate.subtitle) .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(1) HStack(spacing: 12) { Text(String(format: "conf %.2f", candidate.confidence)) Text("ttl \(candidate.ttlSec)s") Text(expiresText(now: Int(Date().timeIntervalSince1970))) } .font(.caption) .foregroundStyle(.secondary) } .padding(.vertical, 4) } private func expiresText(now: Int) -> String { let expiresAt = candidate.createdAt + candidate.ttlSec let remaining = expiresAt - now if remaining <= 0 { return "expired" } if remaining < 60 { return "in \(remaining)s" } return "in \(remaining / 60)m" } } struct CandidatesView_Previews: PreviewProvider { static var previews: some View { CandidatesView() } }