CxLLM-IOS/CxLLMStudio/Views/Models/ModelsView.swift
2026-05-16 10:52:04 -05:00

180 lines
5.8 KiB
Swift

// 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))
}
}