From fbafe896819c75f721356d55423531d20161fc53 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sat, 8 Feb 2025 22:17:14 +0800 Subject: [PATCH 1/3] feat: filter tag content for stream result --- .../OpenAI/StreamService+UpdateResult.swift | 3 +- .../Extensions/String/String+Regex.swift | 34 +++++++++++++++++++ EasydictSwiftTests/Test.swift | 19 +++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Easydict/Swift/Service/OpenAI/StreamService+UpdateResult.swift b/Easydict/Swift/Service/OpenAI/StreamService+UpdateResult.swift index ea9d0809f..4135b5ca6 100644 --- a/Easydict/Swift/Service/OpenAI/StreamService+UpdateResult.swift +++ b/Easydict/Swift/Service/OpenAI/StreamService+UpdateResult.swift @@ -7,6 +7,7 @@ // import Foundation +import RegexBuilder extension StreamService { /// Throttle update result text, avoid update UI too frequently. @@ -52,7 +53,7 @@ extension StreamService { // If error is not nil, means stream is finished. result.isStreamFinished = error != nil - let finalText = resultText?.trim() ?? "" + let finalText = resultText?.filterThinkTagContent().trim() ?? "" let updateCompletion = { [weak result] in guard let result else { return } diff --git a/Easydict/Swift/Utility/Extensions/String/String+Regex.swift b/Easydict/Swift/Utility/Extensions/String/String+Regex.swift index acec725bf..9c1fb517a 100644 --- a/Easydict/Swift/Utility/Extensions/String/String+Regex.swift +++ b/Easydict/Swift/Utility/Extensions/String/String+Regex.swift @@ -7,6 +7,7 @@ // import Foundation +import RegexBuilder extension String { func extract(withPattern pattern: String) -> String? { @@ -24,3 +25,36 @@ extension String { return nil } } + +extension String { + /// Filter ... tag content. + /// Example: + /// - "hello abc" -> " abc" + /// - "hello" -> "" + /// - "helloworld" -> "hello" + /// - "helloworld abc" -> "hello abc" + /// - "no tags here" -> "no tags here" + func filterThinkTagContent() -> String { + filterTagContent("think") + } + + func filterTagContent(_ tag: String) -> String { + let startTag = "<\(tag)>" + let endTag = "" + + // Tag pattern + let tagPattern = Regex { + startTag + ZeroOrMore { + // Match any character (non-greedy) until is found + NegativeLookahead(endTag) + CharacterClass.any + } + // Match the closing tag if it exists + Optionally(endTag) + } + + // Replace all matches with an empty string + return replacing(tagPattern, with: "") + } +} diff --git a/EasydictSwiftTests/Test.swift b/EasydictSwiftTests/Test.swift index e1fe6964b..5ec7d307d 100644 --- a/EasydictSwiftTests/Test.swift +++ b/EasydictSwiftTests/Test.swift @@ -91,3 +91,22 @@ import Translation } #expect(true, "Concurrent test getSelectedText completed without crash") } + +@Test func testFilterThinkTags() { + let testCases: [(input: String, expected: String)] = [ + ("hello world", " world"), + ("helloworld", "world"), + ("hello", ""), + ("helloworld", "hello"), + ("no tags here", "no tags here"), + ("partially closed", ""), + ("onetexttwo", "text"), + ("", ""), + ] + + for (index, testCase) in testCases.enumerated() { + let result = testCase.input.filterThinkTagContent() + print("Test Case \(index + 1): \(result)") + #expect(result == testCase.expected) + } +} From 95ce84aefacfb607a616508c364a87a7257ebd01 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sat, 8 Feb 2025 22:28:35 +0800 Subject: [PATCH 2/3] fix: only filter prefix tag --- .../Swift/Utility/Extensions/String/String+Regex.swift | 9 +++++---- EasydictSwiftTests/Test.swift | 8 +++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Easydict/Swift/Utility/Extensions/String/String+Regex.swift b/Easydict/Swift/Utility/Extensions/String/String+Regex.swift index 9c1fb517a..6895a59b9 100644 --- a/Easydict/Swift/Utility/Extensions/String/String+Regex.swift +++ b/Easydict/Swift/Utility/Extensions/String/String+Regex.swift @@ -27,12 +27,12 @@ extension String { } extension String { - /// Filter ... tag content. + /// Filter ^... tag content. /// Example: - /// - "hello abc" -> " abc" /// - "hello" -> "" - /// - "helloworld" -> "hello" - /// - "helloworld abc" -> "hello abc" + /// - "hello" -> "hello" + /// - "helloworld" -> "world" + /// - "helloworld" -> "helloworld" /// - "no tags here" -> "no tags here" func filterThinkTagContent() -> String { filterTagContent("think") @@ -44,6 +44,7 @@ extension String { // Tag pattern let tagPattern = Regex { + Anchor.startOfSubject startTag ZeroOrMore { // Match any character (non-greedy) until is found diff --git a/EasydictSwiftTests/Test.swift b/EasydictSwiftTests/Test.swift index 5ec7d307d..14b0d6b7f 100644 --- a/EasydictSwiftTests/Test.swift +++ b/EasydictSwiftTests/Test.swift @@ -94,13 +94,11 @@ import Translation @Test func testFilterThinkTags() { let testCases: [(input: String, expected: String)] = [ - ("hello world", " world"), - ("helloworld", "world"), ("hello", ""), - ("helloworld", "hello"), + ("hello", "hello"), + ("helloworld", "world"), + ("helloworld", "helloworld"), ("no tags here", "no tags here"), - ("partially closed", ""), - ("onetexttwo", "text"), ("", ""), ] From 0ae5cdd094af661b92a9ffff42164fcb3fcbcea1 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Mon, 10 Feb 2025 21:11:27 +0800 Subject: [PATCH 3/3] feat: add option to hide think tag content --- Easydict/App/Localizable.xcstrings | 28 +++++++++++++++++++ .../ServiceConfigurationKey.swift | 1 + .../OpenAI/StreamService+UpdateResult.swift | 6 +++- .../Swift/Service/OpenAI/StreamService.swift | 8 ++++++ .../StreamConfigurationView.swift | 12 +++++++- 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 51005a66d..f626948db 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -4672,6 +4672,34 @@ } } }, + "service.configuration.openai.hide_think_tag_content.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hide Tag Content" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skryť obsah Tag" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐藏 标签内容" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "隱藏 標籤內容" + } + } + } + }, "service.configuration.openai.system_prompt.placeholder" : { "localizations" : { "en" : { diff --git a/Easydict/Swift/Feature/Configuration/ServiceConfigurationKey.swift b/Easydict/Swift/Feature/Configuration/ServiceConfigurationKey.swift index e31391c4d..3a155090a 100644 --- a/Easydict/Swift/Feature/Configuration/ServiceConfigurationKey.swift +++ b/Easydict/Swift/Feature/Configuration/ServiceConfigurationKey.swift @@ -51,4 +51,5 @@ enum ServiceConfigurationKey: String { case enableCustomPrompt case systemPrompt case userPrompt + case thinkTag } diff --git a/Easydict/Swift/Service/OpenAI/StreamService+UpdateResult.swift b/Easydict/Swift/Service/OpenAI/StreamService+UpdateResult.swift index 4135b5ca6..7b7c5ba86 100644 --- a/Easydict/Swift/Service/OpenAI/StreamService+UpdateResult.swift +++ b/Easydict/Swift/Service/OpenAI/StreamService+UpdateResult.swift @@ -53,7 +53,11 @@ extension StreamService { // If error is not nil, means stream is finished. result.isStreamFinished = error != nil - let finalText = resultText?.filterThinkTagContent().trim() ?? "" + var finalText = resultText?.trim() ?? "" + + if hideThinkTagContent { + finalText = finalText.filterThinkTagContent().trim() + } let updateCompletion = { [weak result] in guard let result else { return } diff --git a/Easydict/Swift/Service/OpenAI/StreamService.swift b/Easydict/Swift/Service/OpenAI/StreamService.swift index 43308c113..173dc11c7 100644 --- a/Easydict/Swift/Service/OpenAI/StreamService.swift +++ b/Easydict/Swift/Service/OpenAI/StreamService.swift @@ -223,6 +223,14 @@ public class StreamService: QueryService { serviceDefaultsKey(.serviceUsageStatus, defaultValue: .default) } + var thinkTagKey: Defaults.Key { + boolDefaultsKey(.thinkTag, defaultValue: true) + } + + var hideThinkTagContent: Bool { + Defaults[thinkTagKey] + } + // In general, LLM services need to observe these keys to enable validation button. var observeKeys: [Defaults.Key] { [ diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/StreamConfigurationView.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/StreamConfigurationView.swift index a6a22a617..512dd62a5 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/StreamConfigurationView.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/StreamConfigurationView.swift @@ -27,7 +27,8 @@ struct StreamConfigurationView: View { showTranslationToggle: Bool = true, showSentenceToggle: Bool = true, showDictionaryToggle: Bool = true, - showUsageStatusPicker: Bool = true + showUsageStatusPicker: Bool = true, + showThinkTagContent: Bool = true ) { self.service = service @@ -41,6 +42,7 @@ struct StreamConfigurationView: View { self.showSentenceToggle = showSentenceToggle self.showDictionaryToggle = showDictionaryToggle self.showUsageStatusPicker = showUsageStatusPicker + self.showThinkTagSection = showThinkTagContent // Disable user to edit built-in supported models. self.isEditable = service.serviceType() != .builtInAI @@ -66,6 +68,7 @@ struct StreamConfigurationView: View { let showSentenceToggle: Bool let showDictionaryToggle: Bool let showUsageStatusPicker: Bool + let showThinkTagSection: Bool var isEditable = true @@ -176,6 +179,13 @@ struct StreamConfigurationView: View { values: ServiceUsageStatus.allCases ) } + + if showThinkTagSection { + ToggleCell( + titleKey: "service.configuration.openai.hide_think_tag_content.title", + key: service.thinkTagKey + ) + } } } }