From 519abf3cb69e01dc454ef23cff040cb2288e3167 Mon Sep 17 00:00:00 2001 From: Kevin McKee Date: Sat, 17 May 2025 11:29:29 -0700 Subject: [PATCH 1/3] Node search improvements --- Package.swift | 2 +- .../Extensions/Array+Extensions.swift | 6 + .../Model/VimAssistant+Handler.swift | 114 ------------------ Sources/VimAssistant/Model/VimAssistant.swift | 59 +++++++++ .../VimAssistant/Views/VimAssistantView.swift | 9 +- .../Views/VimPredictionView.swift | 58 ++++++++- 6 files changed, 126 insertions(+), 122 deletions(-) delete mode 100644 Sources/VimAssistant/Model/VimAssistant+Handler.swift diff --git a/Package.swift b/Package.swift index 1ed2725..1a00b0e 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/codefiesta/VimKit", from: .init(0, 4, 8)) + .package(url: "../VimKit", from: .init(0, 4, 8)) ], targets: [ .target( diff --git a/Sources/VimAssistant/Extensions/Array+Extensions.swift b/Sources/VimAssistant/Extensions/Array+Extensions.swift index b5e6fec..4bca63f 100644 --- a/Sources/VimAssistant/Extensions/Array+Extensions.swift +++ b/Sources/VimAssistant/Extensions/Array+Extensions.swift @@ -20,3 +20,9 @@ extension Array where Element: Comparable { return indices } } + +extension Array where Element == String { + func containsIgnoringCase(_ element: Element) -> Bool { + contains { $0.caseInsensitiveCompare(element) == .orderedSame } + } +} diff --git a/Sources/VimAssistant/Model/VimAssistant+Handler.swift b/Sources/VimAssistant/Model/VimAssistant+Handler.swift deleted file mode 100644 index 2d11a5d..0000000 --- a/Sources/VimAssistant/Model/VimAssistant+Handler.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// VimAssistant+Handler.swift -// VimAssistant -// -// Created by Kevin McKee -// - -import Foundation -import SwiftData -import VimKit - -public extension VimAssistant { - - struct Handler { - - /// Handles the received - /// - Parameters: - /// - vim: the vim object to use - /// - prediction: the prediction - func handle(vim: Vim, prediction: VimPrediction?) { - guard let prediction, let bestPrediction = prediction.bestPrediction, bestPrediction.confidence >= 0.85 else { return } - let action = bestPrediction.action - let ids = collect(vim: vim, prediction: prediction) - Task { @MainActor in - switch action { - case .hide: - guard ids.isNotEmpty else { return } - await vim.hide(ids: ids) - case .isolate: - guard ids.isNotEmpty else { return } - await vim.isolate(ids: ids) - case .quantify: - // TODO: Probably just emit an event that shows the quantities view - break - case .zoomIn: - vim.zoom() - case .zoomOut: - vim.zoom(out: true) - case .lookLeft: - vim.look(.left) - case .lookRight: - vim.look(.right) - case .lookUp: - vim.look(.up) - case .lookDown: - vim.look(.down) - case .panLeft: - vim.pan(.left) - case .panRight: - vim.pan(.right) - case .panUp: - vim.pan(.up) - case .panDown: - vim.pan(.down) - } - } - } - - private func collect(vim: Vim, prediction: VimPrediction) -> [Int] { - - guard let bestPrediction = prediction.bestPrediction, prediction.entities.isNotEmpty else { return []} - let action = bestPrediction.action - - switch action { - case .hide, .isolate: - guard let db = vim.db, db.nodes.isNotEmpty else { return [] } - let modelContext = ModelContext(db.modelContainer) - - var ids: Set = .init() - - // Fetch all geometry nodes - let nodes = db.nodes - let predicate = Database.Node.predicate(nodes: nodes) - let descriptor = FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.index)]) - guard let results = try? modelContext.fetch(descriptor), results.isNotEmpty else { return [] } - - let categoryNames = prediction.entities.filter{ $0.label == .bimCategory }.map { $0.value } - let familyNames = prediction.entities.filter{ $0.label == .bimFamily }.map { $0.value } - - // Tuple of category names and ids - let categories = results.compactMap{ $0.element?.category?.name }.uniqued().sorted{ $0 < $1 }.map { name in - (name: name, ids: results.filter{ $0.element?.category?.name == name}.compactMap{ Int($0.index) }) - } - - // Tuple of family names and ids - let familes = results.compactMap{ $0.element?.familyName }.uniqued().sorted{ $0 < $1 }.map { name in - (name: name, ids: results.filter{ $0.element?.familyName == name}.compactMap{ Int($0.index) }) - } - - // Collect the ids of the matching categories - for name in categoryNames { - let found = categories.filter{ name.localizedStandardContains($0.name) }.map{ $0.ids }.reduce([], +) - ids.formUnion(found) - } - - // Collect the ids of the matching families - for name in familyNames { - let found = familes.filter{ name.localizedStandardContains($0.name) }.map{ $0.ids }.reduce([], +) - ids.formUnion(found) - } - return ids.sorted() - case .quantify, .zoomIn, .zoomOut, .lookLeft, .lookRight, .lookUp, .lookDown, .panLeft, .panRight, .panUp, .panDown: - return [] - } - - } - } -} - -extension Array where Element == String { - func containsIgnoringCase(_ element: Element) -> Bool { - contains { $0.caseInsensitiveCompare(element) == .orderedSame } - } -} diff --git a/Sources/VimAssistant/Model/VimAssistant.swift b/Sources/VimAssistant/Model/VimAssistant.swift index 32d8df3..bce1685 100644 --- a/Sources/VimAssistant/Model/VimAssistant.swift +++ b/Sources/VimAssistant/Model/VimAssistant.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import SwiftData import VimKit public class VimAssistant: ObservableObject, @unchecked Sendable { @@ -111,4 +112,62 @@ public class VimAssistant: ObservableObject, @unchecked Sendable { return try? JSONDecoder().decode(VimPrediction.self, from: data) } + + /// Handles the specified prediction. + /// - Parameters: + /// - vim: the vim object to update based on the given prediction. + /// - prediction: the prediction to handle + func handle(vim: Vim, prediction: VimPrediction?) async { + guard let prediction, let bestPrediction = prediction.bestPrediction else { return } + let action = bestPrediction.action + switch action { + case .isolate: + let ids = search(vim: vim, in: prediction) + guard ids.isNotEmpty else { return } + await vim.isolate(ids: ids) + case .hide: + let ids = search(vim: vim, in: prediction) + guard ids.isNotEmpty else { return } + await vim.hide(ids: ids) + case .quantify: + break + case .zoomIn: + await vim.zoom() + case .zoomOut: + await vim.zoom(out: true) + case .lookLeft: + await vim.look(.left) + case .lookRight: + await vim.look(.right) + case .lookUp: + await vim.look(.up) + case .lookDown: + await vim.look(.down) + case .panLeft: + await vim.pan(.left) + case .panRight: + await vim.pan(.right) + case .panUp: + await vim.pan(.up) + case .panDown: + await vim.pan(.down) + } + } + + /// Performs a fuzzy search using the levenshtein distance algorithm across the node tree to find the best results. + /// - Parameters: + /// - vim: the vim object to search + /// - prediction: the prediction to extract entity names from + /// - Returns: a set of node ids that match the prediction + private func search(vim: Vim, in prediction: VimPrediction) -> Set { + guard let tree = vim.tree else { return [] } + var ids: Set = .init() + let values = prediction.entities.map{ $0.value } + for entity in prediction.entities { + let searchResults = tree.search(entity.value) + guard let bestResult = searchResults.first else { continue } + ids.formUnion(bestResult.item.ids) + } + return ids + } } diff --git a/Sources/VimAssistant/Views/VimAssistantView.swift b/Sources/VimAssistant/Views/VimAssistantView.swift index 086bd94..373d2bf 100644 --- a/Sources/VimAssistant/Views/VimAssistantView.swift +++ b/Sources/VimAssistant/Views/VimAssistantView.swift @@ -15,8 +15,8 @@ public struct VimAssistantView: View { @State var assistant: VimAssistant = .init() - /// The handler to pass prediction information to. - var handler: VimAssistant.Handler = .init() +// /// The handler to pass prediction information to. +// var handler: VimAssistant.Handler = .init() @State var inputText: String = .empty @@ -48,7 +48,10 @@ public struct VimAssistantView: View { predictionView } .onChange(of: assistant.prediction) { _, prediction in - handler.handle(vim: vim, prediction: prediction) + Task { + await assistant.handle(vim: vim, prediction: prediction) + } +// handler.handle(vim: vim, prediction: prediction) } } diff --git a/Sources/VimAssistant/Views/VimPredictionView.swift b/Sources/VimAssistant/Views/VimPredictionView.swift index c2cedae..0598bd4 100644 --- a/Sources/VimAssistant/Views/VimPredictionView.swift +++ b/Sources/VimAssistant/Views/VimPredictionView.swift @@ -71,7 +71,7 @@ struct VimPredictionView: View { for entity in prediction.entities { let entityText = text[entity.range] var attributedEntityString = AttributedString(entityText) - attributedEntityString.foregroundColor = .cyan + attributedEntityString.foregroundColor = entity.label.color attributedEntityString.underlineStyle = .single attributedEntityString.link = URL(string: "/\(entity.label)/\(entity.value)")! result.replaceSubrange(bounds: entity.range, with: attributedEntityString) @@ -95,15 +95,17 @@ struct VimPredictionView: View { Divider() .fixedSize() - Text("Recognized entities:") - .font(.subheadline).bold() + if prediction.entities.isNotEmpty { + Text("Recognized entities:") + .font(.subheadline).bold() + } ForEach(prediction.entities) { entity in HStack { Text(text[entity.range]) .bold() Text(entity.label.rawValue) .padding(1) - .background(Color.cyan) + .background(entity.label.color) .foregroundStyle(Color.black) .cornerRadius(2) } @@ -133,3 +135,51 @@ struct VimPredictionView: View { let prediction = try! JSONDecoder().decode(VimPrediction.self, from: json.data(using: .utf8)!) VimPredictionView(prediction: prediction, explain: .constant(true)) } + +extension VimPrediction.NerLabel { + + var color: Color { + switch self { + case .person: + .purple + case .organization: + .blue + case .location: + .black + case .date: + .mint + case .time: + .cyan + case .event: + .cyan + case .workOfArt: + .cyan + case .fac: + .cyan + case .gpe: + .cyan + case .language: + .cyan + case .law: + .cyan + case .norp: + .cyan + case .product: + .cyan + case .cardinal: + .blue + case .bimCategory: + .cyan + case .bimFamily: + .orange + case .bimType: + .cyan + case .bimInstance: + .cyan + case .bimLevel: + .yellow + case .bimView: + .cyan + } + } +} From 177d097868031c18703aaba882dd6d1c89c82420 Mon Sep 17 00:00:00 2001 From: Kevin McKee Date: Sat, 17 May 2025 11:34:26 -0700 Subject: [PATCH 2/3] Bumping the kit version and fixing warnings --- Package.swift | 2 +- Sources/VimAssistant/Model/VimAssistant.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 1a00b0e..ac25d06 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "../VimKit", from: .init(0, 4, 8)) + .package(url: "https://github.com/codefiesta/VimKit", from: .init(0, 4, 9)) ], targets: [ .target( diff --git a/Sources/VimAssistant/Model/VimAssistant.swift b/Sources/VimAssistant/Model/VimAssistant.swift index bce1685..c84b917 100644 --- a/Sources/VimAssistant/Model/VimAssistant.swift +++ b/Sources/VimAssistant/Model/VimAssistant.swift @@ -162,7 +162,6 @@ public class VimAssistant: ObservableObject, @unchecked Sendable { private func search(vim: Vim, in prediction: VimPrediction) -> Set { guard let tree = vim.tree else { return [] } var ids: Set = .init() - let values = prediction.entities.map{ $0.value } for entity in prediction.entities { let searchResults = tree.search(entity.value) guard let bestResult = searchResults.first else { continue } From 815a73c64de66d4e3a9a402684aa9b1dc20556ae Mon Sep 17 00:00:00 2001 From: Kevin McKee Date: Sat, 17 May 2025 11:35:46 -0700 Subject: [PATCH 3/3] Removing unused import --- Sources/VimAssistant/Model/VimAssistant.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/VimAssistant/Model/VimAssistant.swift b/Sources/VimAssistant/Model/VimAssistant.swift index c84b917..3f132df 100644 --- a/Sources/VimAssistant/Model/VimAssistant.swift +++ b/Sources/VimAssistant/Model/VimAssistant.swift @@ -7,7 +7,6 @@ import Combine import Foundation -import SwiftData import VimKit public class VimAssistant: ObservableObject, @unchecked Sendable {