CxWebApp/swift-app/Sources/CxWebAppMac/ContentView.swift
CxAI Agent 055e350108
Some checks are pending
build-and-push / image (push) Waiting to run
feat: initial CxWebApp (macOS shell + swift-app wired to CxLLM-SDK)
2026-05-16 14:32:01 -05:00

138 lines
4.6 KiB
Swift

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<WebView.WebAction, Never>
@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<WebView.WebAction, Never>
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()) }
}