180 lines
5.8 KiB
Swift
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))
|
|
}
|
|
}
|