Files
byre/mac/Sources/Byre/Attention.swift
T

150 lines
4.8 KiB
Swift

import AppKit
import SwiftUI
let otherStop = "\u{0}other"
@MainActor
func otherOpen(_ store: Store, _ key: String) -> Bool {
(store.drafts?.hasTextAnswer(key) ?? false) || store.images.has(key)
}
struct AttentionCard<Content: View>: View {
let open: Bool
@ViewBuilder let content: Content
var body: some View {
content
.padding(.horizontal, 12)
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(open ? Palette.warnTint : Palette.warnFaint, in: RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(open ? Palette.warn.opacity(0.55) : Palette.border))
}
}
struct AttentionHeader: View {
let label: String
let when: Date
let open: Bool
var body: some View {
HStack(spacing: 8) {
Text(label)
.font(Fonts.font(11, weight: open ? 700 : 400))
.foregroundStyle(open ? Palette.warn : Palette.muted)
WhenText(when: when)
.font(Fonts.font(11))
.foregroundStyle(Palette.muted)
}
}
}
struct AttentionReply<Leading: View, Trailing: View>: View {
let key: String
let placeholder: String
let busy: Bool
var focus: FocusState<String?>.Binding
let send: () -> Void
@ViewBuilder let leading: Leading
@ViewBuilder let trailing: Trailing
var body: some View {
VStack(alignment: .leading, spacing: 6) {
OtherEntry(key: key, placeholder: placeholder, busy: busy, focus: focus, send: send)
HStack(spacing: 8) {
leading
Spacer(minLength: 0)
trailing
}
.disabled(busy)
}
}
}
private struct OtherEntry: View {
let key: String
let placeholder: String
let busy: Bool
var focus: FocusState<String?>.Binding
let send: () -> Void
@Environment(Store.self) private var store
@FocusState private var editing: Bool
@ViewState private var hover = false
@ViewState private var text = ""
var body: some View {
let focused = focus.wrappedValue == otherStop
Group {
if otherOpen(store, key) {
VStack(alignment: .leading, spacing: 6) {
Thumbnails(key: key)
AgentEditor(text: $text, placeholder: placeholder, key: key, height: 66,
focus: $editing, border: editing ? Palette.accent : Palette.border, send: send)
.disabled(busy)
}
.onChange(of: editing) {
if !editing {
if trimmed(text).isEmpty && !store.images.has(key) && !busy {
store.drafts?.dropTextAnswer(key)
}
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(busy ? 0.6 : 1)
.contentShape(Rectangle())
.focusable(!busy)
.focusEffectDisabled()
.focused(focus, equals: otherStop)
.onTapGesture {
open()
}
.onKeyPress(keys: [.return, .space]) { _ in
open()
return .handled
}
.onHover { hover = $0 }
}
}
.onAppear {
text = store.drafts?.textAnswer(key) ?? ""
}
.onChange(of: text) {
if text != store.drafts?.textAnswer(key) ?? "" {
store.drafts?.setTextAnswer(key, text)
}
store.paneFocus.setDraft(key, !trimmed(text).isEmpty)
}
.onChange(of: store.drafts?.textAnswer(key)) { _, new in
if (new ?? "") != text {
text = new ?? ""
}
}
.onDisappear {
store.paneFocus.setDraft(key, false)
}
}
private func open() {
if busy {
return
}
store.drafts?.setTextAnswer(key, "")
DispatchQueue.main.async {
editing = true
}
}
}