File size: 5,602 Bytes
b8370f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | #if os(macOS)
import SwiftUI
struct ChatTranscriptSearchResults: Equatable {
let messageIDs: [UUID]
init(rows: [ChatTranscriptRow], query: String) {
let query = query.trimmingCharacters(in: .whitespacesAndNewlines)
self.messageIDs = query.isEmpty ? [] : rows.compactMap { row in
guard case let .message(message) = row,
["user", "assistant"].contains(message.role.lowercased()),
ChatMessageVisibleText.visibleText(in: message).localizedStandardContains(query)
else { return nil }
return message.id
}
}
func selection(after current: UUID?, backwards: Bool = false) -> UUID? {
guard !self.messageIDs.isEmpty else { return nil }
guard let current, let index = self.messageIDs.firstIndex(of: current) else {
return backwards ? self.messageIDs.last : self.messageIDs.first
}
let offset = backwards ? self.messageIDs.count - 1 : 1
return self.messageIDs[(index + offset) % self.messageIDs.count]
}
}
struct ChatTranscriptSearch: ViewModifier {
let rows: [ChatTranscriptRow]
let sessionKey: String
let isEnabled: Bool
@Binding var selectedMessageID: UUID?
@Binding var isPresented: Bool
let onSelect: (UUID) -> Void
@State private var query = ""
@FocusState private var isFocused: Bool
func body(content: Content) -> some View {
let results = ChatTranscriptSearchResults(rows: self.rows, query: self.query)
return content
.safeAreaInset(edge: .top, spacing: 0) {
if self.isPresented { self.searchBar(results: results) }
}
.toolbar {
if self.isEnabled {
ToolbarItem(placement: .primaryAction) {
Button {
self.isPresented = true
self.isFocused = true
} label: {
Label("Find in Conversation", systemImage: "magnifyingglass")
}
.keyboardShortcut("f", modifiers: .command)
.help("Find in conversation (⌘F)")
}
}
}
.onChange(of: results) { _, results in
guard self.isPresented else { return }
if !results.messageIDs.contains(where: { $0 == self.selectedMessageID }) {
self.select(results.messageIDs.first)
}
}
.onChange(of: self.sessionKey) { _, _ in self.close() }
.onChange(of: self.isPresented) { _, isPresented in
if !isPresented { self.close() }
}
}
private func searchBar(results: ChatTranscriptSearchResults) -> some View {
HStack(spacing: 10) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField("Find in loaded messages", text: self.$query)
.textFieldStyle(.plain)
.focused(self.$isFocused)
.onAppear { self.isFocused = true }
.onSubmit { self.selectNext(in: results) }
.onExitCommand { self.close() }
.accessibilityIdentifier("chat-transcript-search")
Text(self.resultLabel(results: results))
.font(OpenClawChatTypography.caption)
.foregroundStyle(.secondary)
.monospacedDigit()
.fixedSize()
Button {
self.selectNext(in: results, backwards: true)
} label: {
Label("Previous Match", systemImage: "chevron.up")
}
.keyboardShortcut("g", modifiers: [.command, .shift])
.disabled(results.messageIDs.isEmpty)
.help("Previous match (⇧⌘G)")
Button {
self.selectNext(in: results)
} label: {
Label("Next Match", systemImage: "chevron.down")
}
.keyboardShortcut("g", modifiers: .command)
.disabled(results.messageIDs.isEmpty)
.help("Next match (⌘G)")
Button(action: self.close) {
Label("Close Find", systemImage: "xmark")
}
.help("Close find (Escape)")
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(.bar)
.overlay(alignment: .bottom) {
Divider()
}
}
private func resultLabel(results: ChatTranscriptSearchResults) -> String {
if self.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return "" }
guard let selectedMessageID,
let index = results.messageIDs.firstIndex(of: selectedMessageID)
else { return String(localized: "No matches") }
return String(
format: String(localized: "%lld of %lld"),
Int64(index + 1),
Int64(results.messageIDs.count))
}
private func selectNext(in results: ChatTranscriptSearchResults, backwards: Bool = false) {
self.select(results.selection(after: self.selectedMessageID, backwards: backwards))
}
private func select(_ messageID: UUID?) {
self.selectedMessageID = messageID
if let messageID { self.onSelect(messageID) }
}
private func close() {
self.isPresented = false
self.query = ""
self.selectedMessageID = nil
}
}
#endif
|