Support configuring effort alongside model
This commit is contained in:
@@ -80,6 +80,9 @@ func uploadImages(_ api: API, _ imgs: [PendingImage]) async throws -> [String]?
|
||||
@MainActor
|
||||
var modelChoices: [String: String] = [:]
|
||||
|
||||
@MainActor
|
||||
var effortChoices: [String: String] = [:]
|
||||
|
||||
func vendorOf(_ model: String) -> String {
|
||||
if model.hasPrefix("gpt-") {
|
||||
return "codex"
|
||||
@@ -90,6 +93,18 @@ func vendorOf(_ model: String) -> String {
|
||||
return "claude"
|
||||
}
|
||||
|
||||
func efforts(_ model: String) -> [String] {
|
||||
vendorOf(model) == "codex" ? ["", "low", "medium", "high", "xhigh"] : ["", "low", "medium", "high", "xhigh", "max"]
|
||||
}
|
||||
|
||||
func effortLabel(_ effort: String) -> String {
|
||||
effort.isEmpty ? "default effort" : "\(effort) effort"
|
||||
}
|
||||
|
||||
func effortFor(_ model: String, _ effort: String) -> String {
|
||||
efforts(model).contains(effort) ? effort : ""
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class ImageDrafts {
|
||||
@@ -392,7 +407,9 @@ struct ComposerMenu: View {
|
||||
let photos: Bool
|
||||
var models: [String] = []
|
||||
var selected = ""
|
||||
var effort = ""
|
||||
var choose: @MainActor @Sendable (String) -> Void = { _ in }
|
||||
var chooseEffort: @MainActor @Sendable (String) -> Void = { _ in }
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var picking = false
|
||||
@@ -419,6 +436,12 @@ struct ComposerMenu: View {
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
Picker("Effort for the next turn", selection: Binding(get: { effort }, set: chooseEffort)) {
|
||||
ForEach(efforts(selected), id: \.self) { option in
|
||||
Text(effortLabel(option)).tag(option)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
@@ -548,6 +571,7 @@ struct ComposerView: View {
|
||||
@Environment(\.inTask) private var inTask
|
||||
@ViewState private var text = ""
|
||||
@ViewState private var pick: String?
|
||||
@ViewState private var effortPick: String?
|
||||
@ViewState private var shownOverride: Bool?
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
@@ -579,19 +603,28 @@ struct ComposerView: View {
|
||||
private struct ModelState {
|
||||
let live: AgentSession?
|
||||
let current: String
|
||||
let currentEffort: String
|
||||
let options: [String]
|
||||
let modelKey: String
|
||||
let selected: String
|
||||
let selectedEffort: String
|
||||
|
||||
var changed: Bool {
|
||||
selected != current || selectedEffort != currentEffort
|
||||
}
|
||||
}
|
||||
|
||||
private func modelState() -> ModelState {
|
||||
let live = chat.closed ? chat.session : chat.liveSession
|
||||
let current = live.map { $0.model.isEmpty ? chat.builder.context[$0.id]?.model ?? "" : $0.model } ?? ""
|
||||
let currentEffort = live?.effort ?? ""
|
||||
let offered = live.map { s in models.filter { vendorOf($0) == s.vendor.rawValue } } ?? models
|
||||
let options = !current.isEmpty && !offered.contains(current) ? [current] + offered : offered
|
||||
let modelKey = "message:\(live?.id ?? key)"
|
||||
let fresh = live == nil ? store.defaultModel : options[0]
|
||||
return ModelState(live: live, current: current, options: options, modelKey: modelKey, selected: pick ?? modelChoices[modelKey] ?? (current.isEmpty ? fresh : current))
|
||||
let selected = pick ?? modelChoices[modelKey] ?? (current.isEmpty ? fresh : current)
|
||||
let effort = effortPick ?? effortChoices[modelKey] ?? (live == nil ? store.defaultEffort : currentEffort)
|
||||
return ModelState(live: live, current: current, currentEffort: currentEffort, options: options, modelKey: modelKey, selected: selected, selectedEffort: effortFor(selected, effort))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -668,6 +701,7 @@ struct ComposerView: View {
|
||||
}
|
||||
.onChange(of: m.modelKey) {
|
||||
pick = nil
|
||||
effortPick = nil
|
||||
}
|
||||
.onChange(of: overlays.leadFocus) {
|
||||
if chat.kind == .lead {
|
||||
@@ -702,9 +736,12 @@ struct ComposerView: View {
|
||||
.buttonStyle(WebButtonStyle())
|
||||
.help(shown ? "Hide the message box" : "Show the message box")
|
||||
#if os(iOS)
|
||||
ComposerMenu(key: draftKey, photos: shown, models: chat.team ? [] : m.options, selected: m.selected) { option in
|
||||
ComposerMenu(key: draftKey, photos: shown, models: chat.team ? [] : m.options, selected: m.selected, effort: m.selectedEffort) { option in
|
||||
pick = option
|
||||
modelChoices[m.modelKey] = option
|
||||
} chooseEffort: { option in
|
||||
effortPick = option
|
||||
effortChoices[m.modelKey] = option
|
||||
}
|
||||
#else
|
||||
if !chat.team {
|
||||
@@ -720,6 +757,18 @@ struct ComposerView: View {
|
||||
.pickerStyle(.menu)
|
||||
.fixedSize()
|
||||
.help("Model for the next turn")
|
||||
Picker("", selection: Binding(get: { m.selectedEffort }, set: { v in
|
||||
effortPick = v
|
||||
effortChoices[m.modelKey] = v
|
||||
})) {
|
||||
ForEach(efforts(m.selected), id: \.self) { option in
|
||||
Text(effortLabel(option)).tag(option)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.pickerStyle(.menu)
|
||||
.fixedSize()
|
||||
.help("Effort for the next turn")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -799,9 +848,11 @@ struct ComposerView: View {
|
||||
ids = try await uploadImages(store.api, imgs)
|
||||
uploaded = true
|
||||
}
|
||||
try await chat.send(sessionId: sessionId, text: value, images: ids, model: m.selected != m.current ? m.selected : nil, firstModel: m.selected, sendID: sendID)
|
||||
try await chat.send(sessionId: sessionId, text: value, images: ids, model: m.changed ? m.selected : nil, effort: m.changed ? m.selectedEffort : nil, firstModel: m.selected, firstEffort: m.selectedEffort, sendID: sendID)
|
||||
modelChoices[m.modelKey] = nil
|
||||
effortChoices[m.modelKey] = nil
|
||||
pick = nil
|
||||
effortPick = nil
|
||||
drafts.endSend(sent, true)
|
||||
return
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user