import SwiftUI import Combine /// Top-level view: NavigationSplitView with a CxLLM sidebar. /// The original WebView surface lives in `WebContentView` and is the /// default selection so existing behavior is preserved. struct ContentView: View { let webAction: PassthroughSubject @State private var selection: SidebarItem? = .web var body: some View { NavigationSplitView { List(SidebarItem.allCases, selection: $selection) { item in Label(item.rawValue, systemImage: item.systemImage) .tag(item) } .navigationTitle("CxLLM") .frame(minWidth: 180) } detail: { switch selection ?? .web { case .web: WebContentView(webAction: webAction) case let other: CxLLMSectionView(item: other) } } } } struct WebContentView: View { @EnvironmentObject var settings: AppSettings @StateObject private var health = HealthMonitor() @State private var canGoBack = false @State private var canGoForward = false @State private var isLoading = false @State private var pageTitle = "" let webAction: PassthroughSubject private var url: URL { URL(string: settings.backendURL) ?? URL(string: "about:blank")! } var body: some View { VStack(spacing: 0) { toolbar Divider() WebView( url: url, canGoBack: $canGoBack, canGoForward: $canGoForward, isLoading: $isLoading, pageTitle: $pageTitle, developerExtras: settings.developerExtras, action: webAction ) Divider() statusBar } .frame(minWidth: 800, minHeight: 600) .onAppear { health.start(url: settings.backendURL) } .onChange(of: settings.backendURL) { _ in health.start(url: settings.backendURL) } } // MARK: - private var toolbar: some View { HStack(spacing: 8) { Button { webAction.send(.back) } label: { Image(systemName: "chevron.left") } .disabled(!canGoBack) Button { webAction.send(.forward) } label: { Image(systemName: "chevron.right") } .disabled(!canGoForward) Button { webAction.send(.reload) } label: { Image(systemName: "arrow.clockwise") } Button { webAction.send(.home) } label: { Image(systemName: "house") } Divider().frame(height: 16) statusPill Text(settings.backendURL) .lineLimit(1) .truncationMode(.middle) .foregroundStyle(.secondary) .font(.system(.caption, design: .monospaced)) Spacer() if isLoading { ProgressView().scaleEffect(0.5).frame(width: 16, height: 16) } Button { webAction.send(.copyURL) } label: { Image(systemName: "doc.on.doc") } .help("Copy URL") Button { webAction.send(.openInBrowser) } label: { Image(systemName: "safari") } .help("Open in default browser") } .padding(.horizontal, 12) .padding(.vertical, 8) .buttonStyle(.borderless) } private var statusPill: some View { let (label, color): (String, Color) = { switch health.status { case .unknown: return ("…", .gray) case .up: return ("healthy", .green) case .down: return ("down", .red) } }() return HStack(spacing: 4) { Circle().fill(color).frame(width: 8, height: 8) Text(label).font(.caption) } .padding(.horizontal, 8).padding(.vertical, 2) .background(.quaternary.opacity(0.6), in: Capsule()) .help(health.lastError ?? "Backend reachable") } private var statusBar: some View { HStack(spacing: 12) { Text(pageTitle.isEmpty ? "CxWebApp" : pageTitle) .font(.caption) .lineLimit(1) Spacer() if let t = health.lastChecked { Text("checked " + Self.relative(t)) .font(.caption2) .foregroundStyle(.secondary) } } .padding(.horizontal, 12) .padding(.vertical, 4) } private static let relativeFmt: RelativeDateTimeFormatter = { let f = RelativeDateTimeFormatter(); f.unitsStyle = .short; return f }() private static func relative(_ d: Date) -> String { relativeFmt.localizedString(for: d, relativeTo: Date()) } }