Skip to content

Commit

Permalink
Enable the duplicate ability for custom ai service (#647)
Browse files Browse the repository at this point in the history
* feat: enable custom ai service duplicatable

* fix: service config not updated issue

* fix: service copy  to multi windows and service config issue

* fix: optimize some duplicate code

* fix: endpoint empty error result issue

* fix: miss error alert when openai endpoint is emtpy

* fix: call completion when isStreamFinished is true

* fix: query window service config not changed issue

* feat: optimize some logic

* feat: optimize func name

* fix: loading animation issue and optimize logic

* refactro: rename serviceTypeWithIdIfHave to serviceTypeWithUniqueIdentifier

* style: format code

---------

Co-authored-by: tisfeng <tisfeng@gmail.com>
  • Loading branch information
phlpsong and tisfeng authored Sep 15, 2024
1 parent 2d0cf75 commit 7e9a826
Show file tree
Hide file tree
Showing 27 changed files with 298 additions and 117 deletions.
1 change: 1 addition & 0 deletions Easydict/App/Easydict-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@
#import "MMCrash.h"
#import "EZDetectManager.h"
#import "EZAppleDictionary.h"
#import "EZServiceTypes.h"
32 changes: 32 additions & 0 deletions Easydict/App/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -6240,6 +6240,38 @@
}
}
},
"service.configuration.duplicate" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Duplicate"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "复制"
}
}
}
},
"service.configuration.remove" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Remove"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "删除"
}
}
}
},
"service.configuration.validation_fail" : {
"localizations" : {
"en" : {
Expand Down
14 changes: 10 additions & 4 deletions Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,20 @@ class ShortcutWrapper<T: KeyCombo> {
}
}

func defaultsKey<T>(_ key: StoredKey, serviceType: ServiceType) -> Defaults.Key<T?> {
defaultsKey(key, serviceType: serviceType, defaultValue: nil)
func defaultsKey<T>(_ key: StoredKey, serviceType: ServiceType, id: String) -> Defaults.Key<T?> {
defaultsKey(key, serviceType: serviceType, id: id, defaultValue: nil)
}

func defaultsKey<T: _DefaultsSerializable>(_ key: StoredKey, serviceType: ServiceType, defaultValue: T) -> Defaults
func defaultsKey<T: _DefaultsSerializable>(
_ key: StoredKey,
serviceType: ServiceType,
id: String?,
defaultValue: T
)
-> Defaults
.Key<T> {
Defaults.Key<T>(
storedKey(key, serviceType: serviceType),
storedKey(key, serviceType: serviceType, id: id),
default: defaultValue
)
}
Expand Down
11 changes: 5 additions & 6 deletions Easydict/Swift/Feature/HTTPServer/Vapor/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ func routes(_ app: Application) throws {

app.post("translate") { req async throws -> TranslationResponse in
let request = try req.content.decode(TranslationRequest.self)
let serviceType = ServiceType(rawValue: request.serviceType)
let appleDictionaryNames = request.appleDictionaryNames

guard let service = ServiceTypes.shared().service(withType: serviceType) else {
throw TranslationError.unsupportedServiceType(serviceType.rawValue)
guard let service = ServiceTypes.shared().service(withTypeId: request.serviceType) else {
throw TranslationError.unsupportedServiceType(request.serviceType)
}

if let appleDictionary = service as? AppleDictionary, let appleDictionaryNames {
Expand All @@ -29,7 +28,7 @@ func routes(_ app: Application) throws {
if service.isStream() {
throw TranslationError
.invalidParameter(
"\(serviceType.rawValue) is stream service, which does not support 'translate' API. Please use 'streamTranslate."
"\(request.serviceType) is stream service, which does not support 'translate' API. Please use 'streamTranslate."
)
}

Expand All @@ -53,8 +52,8 @@ func routes(_ app: Application) throws {
let request = try req.content.decode(TranslationRequest.self)
let serviceType = ServiceType(rawValue: request.serviceType)

guard let service = ServiceTypes.shared().service(withType: serviceType) else {
throw TranslationError.unsupportedServiceType(serviceType.rawValue)
guard let service = ServiceTypes.shared().service(withTypeId: request.serviceType) else {
throw TranslationError.unsupportedServiceType(request.serviceType)
}

guard let streamService = service as? LLMStreamService else {
Expand Down
15 changes: 15 additions & 0 deletions Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ class CustomOpenAIService: BaseOpenAIService {

// MARK: Internal

override func serviceTypeWithUniqueIdentifier() -> String {
guard !uuid.isEmpty else {
return ServiceType.customOpenAI.rawValue
}
return "\(ServiceType.customOpenAI.rawValue)#\(uuid)"
}

override func isDuplicatable() -> Bool {
true
}

override func isRemovable(_ type: EZWindowType) -> Bool {
!uuid.isEmpty
}

override func configurationListItems() -> Any {
StreamConfigurationView(
service: self,
Expand Down
5 changes: 5 additions & 0 deletions Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class BaseOpenAIService: LLMStreamService {
completion: @escaping (EZQueryResult, Error?) -> ()
) {
Task {
result.isStreamFinished = false
result.isLoading = true

var resultText = ""
let queryType = self.queryType(text: text, from: from, to: to)

Expand All @@ -38,6 +41,7 @@ public class BaseOpenAIService: LLMStreamService {
// Get final result text
resultText = getFinalResultText(resultText)
updateResultText(resultText, queryType: queryType, error: nil, completion: completion)
result.isLoading = false
result.isStreamFinished = true
} catch {
// For stream requests, certain special cases may be normal for the first part of the data transfer, but the final parsing is incorrect.
Expand All @@ -51,6 +55,7 @@ public class BaseOpenAIService: LLMStreamService {
logError(String(describing: error))
}
updateResultText(text, queryType: queryType, error: err, completion: completion)
result.isLoading = false
result.isStreamFinished = true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extension LLMStreamService {
logInfo("service config changed: \(serviceType().rawValue), windowType: \(windowType.rawValue)")

NotificationCenter.default.postServiceUpdateNotification(
serviceType: serviceType(),
serviceType: serviceTypeWithUniqueIdentifier(),
windowType: windowType,
autoQuery: autoQuery
)
Expand All @@ -87,10 +87,10 @@ extension LLMStreamService {
}

func stringDefaultsKey(_ key: StoredKey, defaultValue: String) -> Defaults.Key<String> {
defaultsKey(key, serviceType: serviceType(), defaultValue: defaultValue)
defaultsKey(key, serviceType: serviceType(), id: uuid, defaultValue: defaultValue)
}

func serviceDefaultsKey<T>(_ key: StoredKey, defaultValue: T) -> Defaults.Key<T> {
defaultsKey(key, serviceType: serviceType(), defaultValue: defaultValue)
defaultsKey(key, serviceType: serviceType(), id: uuid, defaultValue: defaultValue)
}
}
1 change: 1 addition & 0 deletions Easydict/Swift/Service/OpenAI/LLMStreamService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ extension LLMStreamService {
) {
if result.isStreamFinished {
cancelStream()
completion(result, error)
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ extension NSNotification {
@objc
extension NotificationCenter {
func postServiceUpdateNotification(
serviceType: ServiceType = .init(rawValue: ""),
serviceType: String = "",
windowType: EZWindowType = .none,
autoQuery: Bool = false
) {
let userInfo: [String: Any] = [
EZServiceTypeKey: serviceType.rawValue,
EZServiceTypeKey: serviceType,
EZWindowTypeKey: windowType.rawValue,
EZAutoQueryKey: autoQuery,
]
Expand Down
34 changes: 17 additions & 17 deletions Easydict/Swift/Utility/GlobalContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,6 @@ class GlobalContext: NSObject {
updaterDelegate: updaterHelper,
userDriverDelegate: userDriverHelper
)

for service in services {
if let llmService = service as? LLMStreamService {
llmService.setupSubscribers()
}
}
}

// MARK: Public

/// Retrieves the service of the specified type.
///
/// - Parameter type: The type of service to retrieve.
/// - Returns: The service of the specified type.
public func getService(ofType type: ServiceType) -> QueryService? {
services.first(where: { $0.serviceType().rawValue.caseInsensitiveCompare(type.rawValue) == .orderedSame })
}

// MARK: Internal
Expand All @@ -61,6 +45,22 @@ class GlobalContext: NSObject {

let updaterController: SPUStandardUpdaterController

// refresh subscribed services after duplicate service
func reloadLLMServicesSubscribers() {
for service in services {
if let llmService = service as? LLMStreamService {
llmService.cancelSubscribers()
}
}
let allServiceTypes = EZLocalStorage.shared().allServiceTypes(EZWindowType.main)
services = ServiceTypes.shared().services(fromTypes: allServiceTypes)
for service in services {
if let llmService = service as? LLMStreamService {
llmService.setupSubscribers()
}
}
}

// MARK: Private

private let updaterHelper: SPUUpdaterHelper
Expand All @@ -75,5 +75,5 @@ class GlobalContext: NSObject {
For some strange reason, the old service can not be deallocated, this will cause a memory leak, and we also need to cancel old services subscribers.
*/
private let services = EZLocalStorage.shared().allServices(.none)
private var services: [QueryService] = []
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,38 @@ struct ServiceConfigurationSecretSectionView<Content: View>: View {
}

var footer: some View {
Button {
validate()
} label: {
Group {
if viewModel.isValidating {
ProgressView()
.controlSize(.small)
.progressViewStyle(.circular)
} else {
Text("service.configuration.validate")
HStack {
if service.isDuplicatable() {
Button {
service.duplicate()
} label: {
Text("service.configuration.duplicate")
}

if service.isRemovable(service.windowType) {
Button("service.configuration.remove", role: .destructive) {
service.remove()
}
}

Spacer()
}

Button {
validate()
} label: {
Group {
if viewModel.isValidating {
ProgressView()
.controlSize(.small)
.progressViewStyle(.circular)
} else {
Text("service.configuration.validate")
}
}
}
.disabled(viewModel.isValidateBtnDisabled)
}
.disabled(viewModel.isValidateBtnDisabled)
}

var body: some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,48 @@ extension QueryService: ServiceSecretConfigreValidatable {
translate("曾经沧海难为水", from: .simplifiedChinese, to: .english, completion: completion)
}
}

// MARK: - ServiceSecretConfigreDuplicatable

protocol ServiceSecretConfigreDuplicatable {
func duplicate()
func remove()
}

extension ServiceSecretConfigreDuplicatable {
func duplicate() {}
func remove() {}
}

// MARK: - QueryService + ServiceSecretConfigreDuplicatable

extension QueryService: ServiceSecretConfigreDuplicatable {
func duplicate() {
let uuid = UUID().uuidString
let newServiceType = "\(serviceType().rawValue)#\(uuid)"
guard let newService = ServiceTypes.shared().service(withTypeId: newServiceType) else {
return
}
newService.enabled = false
newService.resetServiceResult()
for winType in [EZWindowType.fixed, EZWindowType.main, EZWindowType.mini] {
var allServiceTypes = EZLocalStorage.shared().allServiceTypes(winType)
allServiceTypes.append(newServiceType)
newService.windowType = winType
EZLocalStorage.shared().setService(newService, windowType: winType)
EZLocalStorage.shared().setAllServiceTypes(allServiceTypes, windowType: winType)
NotificationCenter.default.postServiceUpdateNotification(windowType: winType)
}
GlobalContext.shared.reloadLLMServicesSubscribers()
}

func remove() {
for winType in [EZWindowType.fixed, EZWindowType.main, EZWindowType.mini] {
let allServiceTypes = EZLocalStorage.shared().allServiceTypes(winType)
.filter { $0 != serviceTypeWithUniqueIdentifier() }
EZLocalStorage.shared().setAllServiceTypes(allServiceTypes, windowType: winType)
NotificationCenter.default.postServiceUpdateNotification(windowType: winType)
}
GlobalContext.shared.reloadLLMServicesSubscribers()
}
}
Loading

0 comments on commit 7e9a826

Please sign in to comment.