New version v2.3.6
This commit is contained in:
@@ -36,20 +36,37 @@ struct ModelSelectorView: View {
|
||||
@State private var filterTools = false
|
||||
@State private var filterOnline = false
|
||||
@State private var filterImageGen = false
|
||||
@State private var filterThinking = false
|
||||
@State private var keyboardIndex: Int = -1
|
||||
@State private var sortOrder: ModelSortOrder = .default
|
||||
@State private var selectedInfoModel: ModelInfo? = nil
|
||||
|
||||
private var filteredModels: [ModelInfo] {
|
||||
models.filter { model in
|
||||
let q = searchText.lowercased()
|
||||
let filtered = models.filter { model in
|
||||
let matchesSearch = searchText.isEmpty ||
|
||||
model.name.lowercased().contains(searchText.lowercased()) ||
|
||||
model.id.lowercased().contains(searchText.lowercased())
|
||||
model.name.lowercased().contains(q) ||
|
||||
model.id.lowercased().contains(q) ||
|
||||
model.description?.lowercased().contains(q) == true
|
||||
|
||||
let matchesVision = !filterVision || model.capabilities.vision
|
||||
let matchesTools = !filterTools || model.capabilities.tools
|
||||
let matchesOnline = !filterOnline || model.capabilities.online
|
||||
let matchesImageGen = !filterImageGen || model.capabilities.imageGeneration
|
||||
let matchesThinking = !filterThinking || model.capabilities.thinking
|
||||
|
||||
return matchesSearch && matchesVision && matchesTools && matchesOnline && matchesImageGen
|
||||
return matchesSearch && matchesVision && matchesTools && matchesOnline && matchesImageGen && matchesThinking
|
||||
}
|
||||
|
||||
switch sortOrder {
|
||||
case .default:
|
||||
return filtered
|
||||
case .priceLowHigh:
|
||||
return filtered.sorted { $0.pricing.prompt < $1.pricing.prompt }
|
||||
case .priceHighLow:
|
||||
return filtered.sorted { $0.pricing.prompt > $1.pricing.prompt }
|
||||
case .contextHighLow:
|
||||
return filtered.sorted { $0.contextLength > $1.contextLength }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,16 +78,47 @@ struct ModelSelectorView: View {
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding()
|
||||
.onChange(of: searchText) {
|
||||
// Reset keyboard index when search changes
|
||||
keyboardIndex = -1
|
||||
}
|
||||
|
||||
// Filters
|
||||
// Filters + Sort
|
||||
HStack(spacing: 12) {
|
||||
FilterToggle(isOn: $filterVision, icon: "\u{1F441}\u{FE0F}", label: "Vision")
|
||||
FilterToggle(isOn: $filterTools, icon: "\u{1F527}", label: "Tools")
|
||||
FilterToggle(isOn: $filterOnline, icon: "\u{1F310}", label: "Online")
|
||||
FilterToggle(isOn: $filterImageGen, icon: "\u{1F3A8}", label: "Image Gen")
|
||||
FilterToggle(isOn: $filterThinking, icon: "\u{1F9E0}", label: "Thinking")
|
||||
|
||||
Spacer()
|
||||
|
||||
// Sort menu
|
||||
Menu {
|
||||
ForEach(ModelSortOrder.allCases, id: \.rawValue) { order in
|
||||
Button {
|
||||
sortOrder = order
|
||||
keyboardIndex = -1
|
||||
} label: {
|
||||
if sortOrder == order {
|
||||
Label(order.label, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(order.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "arrow.up.arrow.down")
|
||||
Text("Sort")
|
||||
}
|
||||
.font(.caption)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(sortOrder != .default ? Color.blue.opacity(0.2) : Color.gray.opacity(0.1))
|
||||
.foregroundColor(sortOrder != .default ? .blue : .secondary)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
.menuStyle(.borderlessButton)
|
||||
.fixedSize()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 12)
|
||||
@@ -91,13 +139,11 @@ struct ModelSelectorView: View {
|
||||
ModelRowView(
|
||||
model: model,
|
||||
isSelected: model.id == selectedModel?.id,
|
||||
isKeyboardHighlighted: index == keyboardIndex
|
||||
isKeyboardHighlighted: index == keyboardIndex,
|
||||
onSelect: { onSelect(model) },
|
||||
onInfo: { selectedInfoModel = model }
|
||||
)
|
||||
.id(model.id)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onSelect(model)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onChange(of: keyboardIndex) { _, newIndex in
|
||||
@@ -143,20 +189,42 @@ struct ModelSelectorView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Initialize keyboard index to current selection
|
||||
if let selected = selectedModel,
|
||||
let index = filteredModels.firstIndex(where: { $0.id == selected.id }) {
|
||||
keyboardIndex = index
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedInfoModel) { model in
|
||||
ModelInfoView(model: model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sort Order
|
||||
|
||||
enum ModelSortOrder: String, CaseIterable {
|
||||
case `default`
|
||||
case priceLowHigh
|
||||
case priceHighLow
|
||||
case contextHighLow
|
||||
|
||||
var label: LocalizedStringKey {
|
||||
switch self {
|
||||
case .default: return "Default"
|
||||
case .priceLowHigh: return "Price: Low to High"
|
||||
case .priceHighLow: return "Price: High to Low"
|
||||
case .contextHighLow: return "Context: High to Low"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Filter Toggle
|
||||
|
||||
struct FilterToggle: View {
|
||||
@Binding var isOn: Bool
|
||||
let icon: String
|
||||
let label: String
|
||||
let label: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
Button(action: { isOn.toggle() }) {
|
||||
@@ -175,51 +243,74 @@ struct FilterToggle: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Model Row
|
||||
|
||||
struct ModelRowView: View {
|
||||
let model: ModelInfo
|
||||
let isSelected: Bool
|
||||
var isKeyboardHighlighted: Bool = false
|
||||
let onSelect: () -> Void
|
||||
let onInfo: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(model.name)
|
||||
.font(.headline)
|
||||
.foregroundColor(isSelected ? .blue : .primary)
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
// Selectable main content
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(model.name)
|
||||
.font(.headline)
|
||||
.foregroundColor(isSelected ? .blue : .primary)
|
||||
|
||||
Spacer()
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
// Capabilities
|
||||
Text(model.id)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let description = model.description {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Label(model.contextLengthDisplay, systemImage: "text.alignleft")
|
||||
Label(model.promptPriceDisplay + "/1M", systemImage: "dollarsign.circle")
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { onSelect() }
|
||||
|
||||
// Right side: capabilities + info button
|
||||
VStack(alignment: .trailing, spacing: 6) {
|
||||
// Capability icons
|
||||
HStack(spacing: 4) {
|
||||
if model.capabilities.vision { Text("\u{1F441}\u{FE0F}").font(.caption) }
|
||||
if model.capabilities.tools { Text("\u{1F527}").font(.caption) }
|
||||
if model.capabilities.online { Text("\u{1F310}").font(.caption) }
|
||||
if model.capabilities.imageGeneration { Text("\u{1F3A8}").font(.caption) }
|
||||
if model.capabilities.thinking { Text("\u{1F9E0}").font(.caption) }
|
||||
}
|
||||
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.blue)
|
||||
// Info button
|
||||
Button(action: onInfo) {
|
||||
Image(systemName: "info.circle")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Show model info")
|
||||
}
|
||||
|
||||
Text(model.id)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let description = model.description {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Label(model.contextLengthDisplay, systemImage: "text.alignleft")
|
||||
Label(model.promptPriceDisplay + "/1M", systemImage: "dollarsign.circle")
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, isKeyboardHighlighted ? 4 : 0)
|
||||
|
||||
Reference in New Issue
Block a user