import SwiftUI struct SearchView: View { @Bindable var viewModel: BookmarksViewModel @State private var searchText = "" @State private var useSemanticSearch = false @State private var semanticResults: [Bookmark]? = nil @State private var searchTask: Task? @State private var browsingBookmark: Bookmark? var body: some View { NavigationStack { List { ForEach(results) { bookmark in BookmarkListRow( bookmark: bookmark, viewModel: viewModel, onOpen: { browsingBookmark = bookmark } ) } } .listStyle(.plain) .navigationTitle("Search") .toolbar { ToolbarItem(placement: .topBarTrailing) { Toggle(isOn: $useSemanticSearch) { Label("Semantic", systemImage: "sparkles") } .toggleStyle(.button) .onChange(of: useSemanticSearch) { _, _ in scheduleSearch() } } } .overlay { if searchText.isEmpty { ContentUnavailableView("Search Bookmarks", systemImage: "magnifyingglass", description: Text("Search by title, URL, or tag.")) } else if results.isEmpty && !viewModel.isLoading { ContentUnavailableView.search(text: searchText) } if viewModel.isLoading { ProgressView() } } .sensoryFeedback(.selection, trigger: browsingBookmark?.id) .sheet(item: $browsingBookmark) { bookmark in if let url = URL(string: bookmark.url) { BrowserView( url: url, title: bookmark.displayTitle, claude: viewModel.claude, podcastPlayer: viewModel.podcastPlayer ) { await viewModel.archive(bookmark) } } } } .searchable(text: $searchText, prompt: "Titles, URLs, tags…") .onChange(of: searchText) { _, _ in scheduleSearch() } } private var results: [Bookmark] { if let semantic = semanticResults { return semantic } guard !searchText.isEmpty else { return [] } let q = searchText.lowercased() return viewModel.bookmarks.filter { b in b.displayTitle.lowercased().contains(q) || b.url.lowercased().contains(q) || b.tagNames.contains { $0.lowercased().contains(q) } || (b.aiTags ?? []).contains { $0.lowercased().contains(q) } || (b.aiSummary ?? "").lowercased().contains(q) } } private func scheduleSearch() { semanticResults = nil searchTask?.cancel() guard !searchText.isEmpty, useSemanticSearch else { return } let claude = viewModel.claude searchTask = Task { try? await Task.sleep(for: .milliseconds(500)) guard !Task.isCancelled else { return } let q = searchText let all = viewModel.bookmarks do { let ranked = try await claude.semanticSearch(query: q, in: all) await MainActor.run { semanticResults = ranked } } catch {} } } }