Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable the duplicate ability for custom ai service #647

Merged
merged 20 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ac2c37f
feat: enable custom ai service duplicatable
phlpsong Aug 25, 2024
4d602c2
fix: service config not updated issue
phlpsong Aug 25, 2024
0f259a4
Merge branch 'dev' into phillip/custom-ai-service-duplicatable
tisfeng Aug 26, 2024
fc29714
fix: service copy to multi windows and service config issue
phlpsong Aug 30, 2024
9a87e56
fix: optimize some duplicate code
phlpsong Aug 30, 2024
848d3a3
fix: endpoint empty error result issue
phlpsong Aug 30, 2024
8733c9b
Merge branch 'dev' into phillip/custom-ai-service-duplicatable
phlpsong Sep 1, 2024
2823003
Merge branch 'dev' into phillip/custom-ai-service-duplicatable
tisfeng Sep 3, 2024
7621e9e
fix: miss error alert when openai endpoint is emtpy
tisfeng Sep 3, 2024
85dc345
fix: call completion when isStreamFinished is true
tisfeng Sep 4, 2024
b3fe64f
fix: query window service config not changed issue
phlpsong Sep 6, 2024
1adfdaf
feat: optimize some logic
phlpsong Sep 6, 2024
c79e4f4
Merge branch 'dev' into phillip/custom-ai-service-duplicatable
tisfeng Sep 7, 2024
ceaf358
feat: optimize func name
phlpsong Sep 7, 2024
0f16b3e
Merge branch 'dev' into phillip/custom-ai-service-duplicatable
tisfeng Sep 8, 2024
b11ee1a
fix: loading animation issue and optimize logic
phlpsong Sep 8, 2024
ff3c75e
Merge branch 'dev' into phillip/custom-ai-service-duplicatable
tisfeng Sep 10, 2024
4ff5713
Merge branch 'dev' into phillip/custom-ai-service-duplicatable
tisfeng Sep 15, 2024
b41aa9f
refactro: rename serviceTypeWithIdIfHave to serviceTypeWithUniqueIden…
tisfeng Sep 15, 2024
cb002dc
style: format code
tisfeng Sep 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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