Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3d8719874 | ||
|
|
2e1b454c09 |
@@ -225,52 +225,71 @@ final class BookmarksViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func enrichAll() async {
|
func enrichAll() async {
|
||||||
let toEnrich = bookmarks.indices.filter { bookmarks[$0].aiSummary == nil }
|
// Track ids, not indices. Each iteration awaits the network, and
|
||||||
|
// `bookmarks` is replaced wholesale by loads, searches and filter
|
||||||
|
// toggles — an index captured before the await can be out of range by
|
||||||
|
// the time it is used.
|
||||||
|
let toEnrich = bookmarks.filter { $0.aiSummary == nil }.map(\.id)
|
||||||
guard !toEnrich.isEmpty else { return }
|
guard !toEnrich.isEmpty else { return }
|
||||||
enrichmentProgress = 0
|
enrichmentProgress = 0
|
||||||
for (done, i) in toEnrich.enumerated() {
|
var completed = 0
|
||||||
|
for id in toEnrich {
|
||||||
|
guard let bookmark = bookmarks.first(where: { $0.id == id }) else { continue }
|
||||||
do {
|
do {
|
||||||
let (summary, tags) = try await claude.enrich(bookmark: bookmarks[i])
|
let (summary, tags) = try await claude.enrich(bookmark: bookmark)
|
||||||
bookmarks[i].aiSummary = summary
|
apply(summary: summary, tags: tags, toId: id)
|
||||||
bookmarks[i].aiTags = tags
|
completed += 1
|
||||||
saveAIData(for: bookmarks[i])
|
enrichmentProgress = Double(completed) / Double(toEnrich.count)
|
||||||
let enriched = bookmarks[i]
|
|
||||||
Task { await SpotlightIndexer.index([enriched]) }
|
|
||||||
enrichmentProgress = Double(done + 1) / Double(toEnrich.count)
|
|
||||||
} catch {
|
} catch {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let enriched = toEnrich.count - bookmarks.indices.filter { bookmarks[$0].aiSummary == nil }.count
|
if completed > 0 { Analytics.track("enrich.completed", ["count": completed]) }
|
||||||
if enriched > 0 { Analytics.track("enrich.completed", ["count": enriched]) }
|
|
||||||
enrichmentProgress = 0
|
enrichmentProgress = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Silently enrich new bookmarks that lack summaries (background, non-blocking)
|
/// Write an enrichment result back by id, skipping it if the bookmark is no
|
||||||
private func startEnrichment() {
|
/// longer loaded (deleted, archived, or filtered away mid-flight).
|
||||||
enrichTask?.cancel()
|
private func apply(summary: String, tags: [String], toId id: Int) {
|
||||||
enrichTask = Task { [weak self] in
|
guard let i = bookmarks.firstIndex(where: { $0.id == id }) else {
|
||||||
guard let self else { return }
|
Log.ai.notice("enrich result dropped: id=\(id, privacy: .public) no longer loaded")
|
||||||
let indices = await MainActor.run { bookmarks.indices.filter { bookmarks[$0].aiSummary == nil }.prefix(5) }
|
return
|
||||||
Log.ai.info("startEnrichment: \(indices.count, privacy: .public) to enrich")
|
}
|
||||||
for i in indices {
|
|
||||||
guard !Task.isCancelled else { return }
|
|
||||||
let claude = await MainActor.run(body: { self.claude })
|
|
||||||
do {
|
|
||||||
let bm = await MainActor.run { bookmarks[i] }
|
|
||||||
Log.ai.debug("startEnrichment enriching id=\(bm.id, privacy: .public)")
|
|
||||||
let (summary, tags) = try await claude.enrich(bookmark: bm)
|
|
||||||
Log.ai.debug("startEnrichment done id=\(bm.id, privacy: .public)")
|
|
||||||
await MainActor.run {
|
|
||||||
guard i < bookmarks.count else { return }
|
|
||||||
bookmarks[i].aiSummary = summary
|
bookmarks[i].aiSummary = summary
|
||||||
bookmarks[i].aiTags = tags
|
bookmarks[i].aiTags = tags
|
||||||
saveAIData(for: bookmarks[i])
|
saveAIData(for: bookmarks[i])
|
||||||
let enriched = bookmarks[i]
|
let enriched = bookmarks[i]
|
||||||
Task { await SpotlightIndexer.index([enriched]) }
|
Task { await SpotlightIndexer.index([enriched]) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Silently enrich new bookmarks that lack summaries (background, non-blocking)
|
||||||
|
//
|
||||||
|
// Everything here is keyed by bookmark id rather than array index. This loop
|
||||||
|
// awaits a network call per bookmark, and `bookmarks` gets replaced under it
|
||||||
|
// by loads, searches and the unread filter — an index captured up front used
|
||||||
|
// to crash on the read (`bookmarks[i]`), which the write path already
|
||||||
|
// guarded against but the read did not.
|
||||||
|
private func startEnrichment() {
|
||||||
|
enrichTask?.cancel()
|
||||||
|
enrichTask = Task { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let ids = await MainActor.run {
|
||||||
|
bookmarks.filter { $0.aiSummary == nil }.prefix(5).map(\.id)
|
||||||
|
}
|
||||||
|
Log.ai.info("startEnrichment: \(ids.count, privacy: .public) to enrich")
|
||||||
|
for id in ids {
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
let claude = await MainActor.run(body: { self.claude })
|
||||||
|
guard let bm = await MainActor.run(body: {
|
||||||
|
bookmarks.first { $0.id == id }
|
||||||
|
}) else { continue }
|
||||||
|
do {
|
||||||
|
Log.ai.debug("startEnrichment enriching id=\(id, privacy: .public)")
|
||||||
|
let (summary, tags) = try await claude.enrich(bookmark: bm)
|
||||||
|
Log.ai.debug("startEnrichment done id=\(id, privacy: .public)")
|
||||||
|
await MainActor.run { apply(summary: summary, tags: tags, toId: id) }
|
||||||
} catch {
|
} catch {
|
||||||
Log.ai.error("startEnrichment failed idx=\(i, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
Log.ai.error("startEnrichment failed id=\(id, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,23 @@ struct SpotlightRetrievalTests {
|
|||||||
"url did not survive the round trip: \(hit.url)")
|
"url did not survive the round trip: \(hit.url)")
|
||||||
#expect(hit.host == "github.com", "host was \(hit.host)")
|
#expect(hit.host == "github.com", "host was \(hit.host)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func indexedSourceIsRetrievable() async throws {
|
||||||
|
let source = IngestedSource(
|
||||||
|
kind: .text,
|
||||||
|
title: "Wqvbnmtest meeting notes",
|
||||||
|
bodyText: "A distinctive probe document about wqvbnmtest.",
|
||||||
|
tags: ["wqvbnmtest"]
|
||||||
|
)
|
||||||
|
await SourceSpotlightIndexer.index([source])
|
||||||
|
var hits: [RetrievedBookmark] = []
|
||||||
|
for _ in 0..<20 {
|
||||||
|
try? await Task.sleep(for: .milliseconds(700))
|
||||||
|
hits = await SpotlightBookmarkSearch.run(query: "wqvbnmtest", limit: 5)
|
||||||
|
if !hits.isEmpty { break }
|
||||||
|
}
|
||||||
|
await SourceSpotlightIndexer.remove(ids: [source.id])
|
||||||
|
#expect(!hits.isEmpty, "Spotlight returned no hits for an indexed source")
|
||||||
|
#expect(hits.first?.title.contains("Wqvbnmtest") == true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user