// CxLLM Studio — iOS Application // Views/Models/ModelsView.swift — CxModel browser for iOS import SwiftUI import CxCode import CxAWS struct ModelsView: View { @Environment(AppState.self) private var appState @Environment(CxModelController.self) private var modelController var body: some View { List { ForEach(CxModelSlot.allCases) { slot in NavigationLink { ModelDetailView(slot: slot) } label: { ModelRow(slot: slot) } } } } } struct ModelRow: View { let slot: CxModelSlot var body: some View { HStack(spacing: 12) { Image(systemName: slot.icon) .font(.title2) .foregroundStyle(tierColor(slot.tier)) .frame(width: 36) VStack(alignment: .leading, spacing: 2) { Text(slot.codename) .font(.headline) Text(slot.rawValue) .font(.caption) .foregroundStyle(.secondary) } Spacer() Text(slot.tier) .font(.caption2) .padding(.horizontal, 8) .padding(.vertical, 3) .background(tierColor(slot.tier).opacity(0.15)) .clipShape(Capsule()) } } private func tierColor(_ tier: String) -> Color { switch tier { case "fast": return .green case "balanced": return .blue case "premium": return .purple case "safety": return .orange case "ultra": return .red default: return .gray } } } struct ModelDetailView: View { let slot: CxModelSlot @Environment(AppState.self) private var appState @State private var testPrompt = "Hello, introduce yourself in one sentence." @State private var testResult = "" @State private var isTesting = false @Environment(GatewayService.self) private var gateway var body: some View { List { Section("Model Info") { LabeledContent("Codename", value: slot.codename) LabeledContent("Slot", value: slot.rawValue) LabeledContent("Provider", value: slot.provider) LabeledContent("Model", value: slot.defaultModel) LabeledContent("Tier", value: slot.tier) } Section("Capabilities") { FlowLayout(spacing: 6) { ForEach(slot.capabilities, id: \.self) { cap in Text(cap) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 4) .background(.blue.opacity(0.1)) .clipShape(Capsule()) } } } Section("Recommendation") { Text(slot.recommendation) .font(.callout) .foregroundStyle(.secondary) } Section("Test Inference") { TextField("Prompt", text: $testPrompt, axis: .vertical) .lineLimit(2...4) Button { Task { await testInference() } } label: { Label(isTesting ? "Testing..." : "Run Test", systemImage: "play.fill") } .disabled(isTesting) if !testResult.isEmpty { Text(testResult) .font(.callout) .textSelection(.enabled) } } } .navigationTitle(slot.codename) } private func testInference() async { isTesting = true testResult = "" do { let sessionId = try await gateway.createSession(model: slot.rawValue) let response = try await gateway.sendMessage(sessionId: sessionId, message: testPrompt, model: slot.rawValue) testResult = response.reply } catch { testResult = "Error: \(error.localizedDescription)" } isTesting = false } } struct FlowLayout: Layout { var spacing: CGFloat = 6 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let result = arrangeSubviews(proposal: proposal, subviews: subviews) return result.size } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let result = arrangeSubviews(proposal: proposal, subviews: subviews) for (index, position) in result.positions.enumerated() { subviews[index].place(at: CGPoint(x: bounds.minX + position.x, y: bounds.minY + position.y), proposal: ProposedViewSize(result.sizes[index])) } } private func arrangeSubviews(proposal: ProposedViewSize, subviews: Subviews) -> (positions: [CGPoint], sizes: [CGSize], size: CGSize) { let maxWidth = proposal.width ?? .infinity var positions: [CGPoint] = [] var sizes: [CGSize] = [] var x: CGFloat = 0 var y: CGFloat = 0 var rowHeight: CGFloat = 0 var maxX: CGFloat = 0 for subview in subviews { let size = subview.sizeThatFits(.unspecified) if x + size.width > maxWidth && x > 0 { x = 0 y += rowHeight + spacing rowHeight = 0 } positions.append(CGPoint(x: x, y: y)) sizes.append(size) rowHeight = max(rowHeight, size.height) x += size.width + spacing maxX = max(maxX, x) } return (positions, sizes, CGSize(width: maxX, height: y + rowHeight)) } }