150 lines
4.8 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|