openclaw / apps /shared /OpenClawKit /Sources /OpenClawChatUI /ChatTranscriptSearch.swift
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
b8370f5 verified
Raw
History Blame Contribute Delete
5.6 kB
#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