224 lines
8.5 KiB
Swift
224 lines
8.5 KiB
Swift
//
|
|
// ModelInfoView.swift
|
|
// oAI
|
|
//
|
|
// Rich model information modal
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct ModelInfoView: View {
|
|
let model: ModelInfo
|
|
|
|
@Environment(\.dismiss) var dismiss
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Header
|
|
HStack {
|
|
Text("Model Info")
|
|
.font(.system(size: 18, weight: .bold))
|
|
Spacer()
|
|
Button { dismiss() } label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.title2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.keyboardShortcut(.escape, modifiers: [])
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.top, 20)
|
|
.padding(.bottom, 12)
|
|
|
|
Divider()
|
|
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
// Overview
|
|
sectionHeader("Overview")
|
|
infoRow("Name", model.name)
|
|
infoRow("ID", model.id)
|
|
if let provider = model.topProvider {
|
|
infoRow("Provider", provider)
|
|
}
|
|
if let desc = model.description {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("Description")
|
|
.font(.subheadline.weight(.medium))
|
|
.foregroundColor(.secondary)
|
|
Text(desc)
|
|
.font(.body)
|
|
.foregroundColor(.primary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.textSelection(.enabled)
|
|
}
|
|
.padding(.leading, 4)
|
|
}
|
|
|
|
Divider()
|
|
|
|
// Pricing
|
|
sectionHeader("Pricing")
|
|
infoRow("Input", model.promptPriceDisplay + " / 1M tokens")
|
|
infoRow("Output", model.completionPriceDisplay + " / 1M tokens")
|
|
|
|
if model.pricing.prompt > 0 {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("Cost Examples")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
HStack(spacing: 16) {
|
|
costExample(label: "1K tokens", inputTokens: 1_000)
|
|
costExample(label: "10K tokens", inputTokens: 10_000)
|
|
costExample(label: "100K tokens", inputTokens: 100_000)
|
|
}
|
|
}
|
|
.padding(.leading, 4)
|
|
}
|
|
|
|
Divider()
|
|
|
|
// Context Window
|
|
sectionHeader("Context Window")
|
|
infoRow("Max Tokens", model.contextLength.formatted())
|
|
|
|
if model.contextLength > 0 {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
let maxContext = 2_000_000.0
|
|
let fraction = min(Double(model.contextLength) / maxContext, 1.0)
|
|
ZStack(alignment: .leading) {
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color.gray.opacity(0.2))
|
|
.frame(height: 16)
|
|
GeometryReader { geo in
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color.blue)
|
|
.frame(width: geo.size.width * fraction, height: 16)
|
|
}
|
|
.frame(height: 16)
|
|
}
|
|
Text(model.contextLengthDisplay + " tokens")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.padding(.leading, 4)
|
|
}
|
|
|
|
Divider()
|
|
|
|
// Capabilities
|
|
sectionHeader("Capabilities")
|
|
HStack(spacing: 12) {
|
|
capabilityBadge(icon: "eye.fill", label: "Vision", active: model.capabilities.vision)
|
|
capabilityBadge(icon: "wrench.fill", label: "Tools", active: model.capabilities.tools)
|
|
capabilityBadge(icon: "globe", label: "Online", active: model.capabilities.online)
|
|
capabilityBadge(icon: "photo.fill", label: "Image Gen", active: model.capabilities.imageGeneration)
|
|
}
|
|
|
|
// Architecture (if available)
|
|
if let arch = model.architecture {
|
|
Divider()
|
|
sectionHeader("Architecture")
|
|
if let modality = arch.modality {
|
|
infoRow("Modality", modality)
|
|
}
|
|
if let tokenizer = arch.tokenizer {
|
|
infoRow("Tokenizer", tokenizer)
|
|
}
|
|
if let instructType = arch.instructType {
|
|
infoRow("Instruct Type", instructType)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.vertical, 16)
|
|
}
|
|
|
|
Divider()
|
|
|
|
// Bottom bar
|
|
HStack {
|
|
Spacer()
|
|
Button("Done") { dismiss() }
|
|
.keyboardShortcut(.return, modifiers: [])
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.regular)
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.vertical, 12)
|
|
}
|
|
.frame(minWidth: 550, idealWidth: 650, minHeight: 550, idealHeight: 750)
|
|
}
|
|
|
|
// MARK: - Layout Helpers
|
|
|
|
private func sectionHeader(_ title: String) -> some View {
|
|
Text(title)
|
|
.font(.system(size: 13, weight: .semibold))
|
|
.foregroundStyle(.secondary)
|
|
.textCase(.uppercase)
|
|
}
|
|
|
|
private func infoRow(_ label: String, _ value: String) -> some View {
|
|
HStack {
|
|
Text(label)
|
|
.font(.body)
|
|
Spacer()
|
|
Text(value)
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
.textSelection(.enabled)
|
|
}
|
|
.padding(.leading, 4)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func costExample(label: String, inputTokens: Int) -> some View {
|
|
let cost = (Double(inputTokens) * model.pricing.prompt / 1_000_000) +
|
|
(Double(inputTokens) * model.pricing.completion / 1_000_000)
|
|
VStack(spacing: 2) {
|
|
Text(label)
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
Text(String(format: "$%.4f", cost))
|
|
.font(.caption.monospacedDigit())
|
|
.foregroundColor(.primary)
|
|
}
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Color.gray.opacity(0.1))
|
|
.cornerRadius(4)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func capabilityBadge(icon: String, label: String, active: Bool) -> some View {
|
|
VStack(spacing: 4) {
|
|
Image(systemName: icon)
|
|
.font(.title3)
|
|
.foregroundColor(active ? .blue : .gray.opacity(0.4))
|
|
Text(label)
|
|
.font(.caption2)
|
|
.foregroundColor(active ? .primary : .secondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 8)
|
|
.background(active ? Color.blue.opacity(0.1) : Color.gray.opacity(0.05))
|
|
.cornerRadius(8)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ModelInfoView(model: ModelInfo(
|
|
id: "anthropic/claude-sonnet-4",
|
|
name: "Claude Sonnet 4",
|
|
description: "Balanced intelligence and speed. This is a longer description to test how the modal handles multi-line text that wraps across several lines in the description field.",
|
|
contextLength: 200_000,
|
|
pricing: .init(prompt: 3.0, completion: 15.0),
|
|
capabilities: .init(vision: true, tools: true, online: false),
|
|
architecture: .init(tokenizer: "claude", instructType: "claude", modality: "text+image->text"),
|
|
topProvider: "anthropic"
|
|
))
|
|
}
|