Unify attention cards
This commit is contained in:
@@ -6,7 +6,6 @@ typealias Attachment = Components.Schemas.Attachment
|
||||
typealias InboxResponseCreate = Components.Schemas.InboxResponseCreate
|
||||
|
||||
private let delegateText = "Use your judgment for this decision and continue within the existing task instructions."
|
||||
private let otherStop = "\u{0}other"
|
||||
private let httpURL = Re(#"^https?://"#, caseless: true)
|
||||
|
||||
private func numberChoices(_ blocks: [MDBlock], _ next: inout Int) -> [MDBlock] {
|
||||
@@ -80,7 +79,7 @@ private final class CardModel {
|
||||
}
|
||||
|
||||
var key: String {
|
||||
"inbox:\(item.id)"
|
||||
item.id
|
||||
}
|
||||
|
||||
var options: [String] {
|
||||
@@ -120,7 +119,7 @@ private final class CardModel {
|
||||
}
|
||||
|
||||
var editorOpen: Bool {
|
||||
(store.drafts?.hasTextAnswer(item.id) ?? false) || hasImages
|
||||
otherOpen(store, key)
|
||||
}
|
||||
|
||||
func toggle(_ text: String, _ on: Bool) {
|
||||
@@ -213,13 +212,7 @@ struct QuestionCard: View {
|
||||
VStack(spacing: 0) {
|
||||
if let model {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Text("question\(model.answered ? " · \(item.state.rawValue)" : "")")
|
||||
Spacer(minLength: 0)
|
||||
Text(ago(item.createdAt)).help(isoText(item.createdAt))
|
||||
}
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.muted)
|
||||
AttentionHeader(label: "question", when: item.createdAt, open: !model.answered)
|
||||
CardBody(model: model, focus: $focus)
|
||||
Thread(item: item)
|
||||
if !model.answered {
|
||||
@@ -452,139 +445,29 @@ private struct CardActions: View {
|
||||
|
||||
var body: some View {
|
||||
let disabled = model.typed.isEmpty && !model.hasImages && (!model.multiple || model.checked.isEmpty)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
TextEntry(model: model, focus: focus) {
|
||||
model.submitQuestion()
|
||||
AttentionReply(key: model.key, placeholder: model.multiple ? "Your answer, a question back, or new instructions" : "Your answer, or notes to send with a choice you click", busy: model.busy, focus: focus, send: model.submitQuestion) {
|
||||
Button("Let the agent decide") {
|
||||
model.submit(InboxResponseCreate(itemId: model.item.id, _type: .other, text: delegateText))
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
Button("Let the agent decide") {
|
||||
model.submit(InboxResponseCreate(itemId: model.item.id, _type: .other, text: delegateText))
|
||||
.buttonStyle(WebButtonStyle())
|
||||
if model.again {
|
||||
Button("Cancel") {
|
||||
model.again = false
|
||||
}
|
||||
.buttonStyle(WebButtonStyle())
|
||||
if model.again {
|
||||
Button("Cancel") {
|
||||
model.again = false
|
||||
}
|
||||
.buttonStyle(WebButtonStyle())
|
||||
} else {
|
||||
Button("Ignore") {
|
||||
model.dismiss()
|
||||
}
|
||||
.buttonStyle(WebButtonStyle())
|
||||
.help("Dismiss this question without answering it; the agent is not told")
|
||||
}
|
||||
Spacer()
|
||||
Button("Submit") {
|
||||
model.submitQuestion()
|
||||
}
|
||||
.buttonStyle(WebButtonStyle(primary: true))
|
||||
.disabled(disabled)
|
||||
}
|
||||
.disabled(model.busy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TextEntry: View {
|
||||
let model: CardModel
|
||||
var focus: FocusState<String?>.Binding
|
||||
let go: () -> Void
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@Environment(\.paneAccent) private var accent
|
||||
@FocusState private var editing: Bool
|
||||
@ViewState private var hover = false
|
||||
|
||||
var body: some View {
|
||||
let id = model.item.id
|
||||
let key = model.key
|
||||
let text = Binding(get: { store.drafts?.textAnswer(id) ?? "" }, set: { v in
|
||||
store.drafts?.setTextAnswer(id, v)
|
||||
store.paneFocus.setDraft(key, !trimmed(v).isEmpty)
|
||||
})
|
||||
let focused = focus.wrappedValue == otherStop
|
||||
Group {
|
||||
if model.editorOpen {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Thumbnails(key: key)
|
||||
TextEditor(text: text)
|
||||
.font(Fonts.font(13))
|
||||
.scrollContentBackground(.hidden)
|
||||
.writingToolsBehavior(.disabled)
|
||||
.focused($editing)
|
||||
.disabled(model.busy)
|
||||
.onKeyPress(.return, phases: .down) { press in
|
||||
let marked = (NSApp.keyWindow?.firstResponder as? NSTextView)?.hasMarkedText() ?? false
|
||||
if press.modifiers.contains(.shift) || marked {
|
||||
return .ignored
|
||||
}
|
||||
go()
|
||||
return .handled
|
||||
}
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 6)
|
||||
.frame(height: 66)
|
||||
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(alignment: .topLeading) {
|
||||
if text.wrappedValue.isEmpty {
|
||||
Text(model.multiple ? "Your answer, a question back, or new instructions" : "Your answer, or notes to send with a choice you click")
|
||||
.foregroundStyle(Palette.muted)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(editing ? Palette.accent : Palette.border))
|
||||
.imageInput(key)
|
||||
}
|
||||
.onChange(of: editing) {
|
||||
if !editing {
|
||||
if trimmed(text.wrappedValue).isEmpty && !model.hasImages && !model.busy {
|
||||
store.drafts?.dropTextAnswer(id)
|
||||
}
|
||||
store.drafts?.flush()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Other…")
|
||||
.foregroundStyle(Palette.muted)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(hover || focused ? Palette.accent : Palette.border))
|
||||
.overlay {
|
||||
if focused {
|
||||
RoundedRectangle(cornerRadius: 6).stroke(Palette.accent, lineWidth: 2).padding(-2)
|
||||
}
|
||||
}
|
||||
.opacity(model.busy ? 0.6 : 1)
|
||||
.contentShape(Rectangle())
|
||||
.focusable(!model.busy)
|
||||
.focusEffectDisabled()
|
||||
.focused(focus, equals: otherStop)
|
||||
.onTapGesture {
|
||||
open()
|
||||
}
|
||||
.onKeyPress(keys: [.return, .space]) { _ in
|
||||
open()
|
||||
return .handled
|
||||
}
|
||||
.onHover { hover = $0 }
|
||||
Button("Ignore") {
|
||||
model.dismiss()
|
||||
}
|
||||
.buttonStyle(WebButtonStyle())
|
||||
.help("Dismiss this question without answering it; the agent is not told")
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
store.paneFocus.setDraft(key, false)
|
||||
}
|
||||
}
|
||||
|
||||
private func open() {
|
||||
if model.busy {
|
||||
return
|
||||
}
|
||||
store.drafts?.setTextAnswer(model.item.id, "")
|
||||
DispatchQueue.main.async {
|
||||
editing = true
|
||||
} trailing: {
|
||||
Button("Submit") {
|
||||
model.submitQuestion()
|
||||
}
|
||||
.buttonStyle(WebButtonStyle(primary: true))
|
||||
.disabled(disabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user