Initial commit
This commit is contained in:
223
oAI/Views/Screens/ModelInfoView.swift
Normal file
223
oAI/Views/Screens/ModelInfoView.swift
Normal file
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// 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"
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user