-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implement CMM-739: Generate post excerpt #24852
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
Changes from all commits
f955c5d
459b548
405909b
560c62f
e568066
a0a852c
6011562
d740fbd
bc1b7a9
fd252ad
67a8e07
dec2b53
24ab67c
1c643f0
f5ed66b
7918244
821d52b
c8c149f
5beb232
0c3bd21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import Foundation | ||
import FoundationModels | ||
|
||
public enum LanguageModelHelper { | ||
public static var isSupported: Bool { | ||
guard #available(iOS 26, *) else { return false } | ||
switch SystemLanguageModel.default.availability { | ||
case .available: | ||
return true | ||
case .unavailable(let reason): | ||
switch reason { | ||
case .appleIntelligenceNotEnabled, .modelNotReady: | ||
return true | ||
case .deviceNotEligible: | ||
return false | ||
@unknown default: | ||
return false | ||
} | ||
} | ||
} | ||
|
||
public static var generateExcerptInstructions: String { | ||
""" | ||
Generate exactly 3 excerpts for the blog post and follow the instructions from the prompt regarding the length and the style. | ||
|
||
CRITICAL CONSTRAINTS: | ||
• Each excerpt MUST follow the style and the length requirements | ||
|
||
EXCERPT BEST PRACTICES: | ||
* Follow the best practices for post excerpts esteblished in the WordPress ecosystem | ||
• Include the post's main value proposition | ||
• Use active voice (avoid "is", "are", "was", "were" when possible) | ||
• End with implicit promise of more information | ||
• Do not use ellipsis (...) at the end | ||
* Focus on value, not summary | ||
* Include strategic keywords naturall | ||
* Write independently from the introduction – excerpt shouldn't just duplicate your opening paragraph. While your introduction eases readers into the topic, your excerpt needs to work as standalone copy that makes sense out of context—whether it appears in search results, social media cards, or email newsletters. | ||
|
||
VARIATION GUIDELINES: | ||
Excerpt 1: Open with a question that addresses reader's problem | ||
Excerpt 2: Start with a bold statement or surprising fact | ||
Excerpt 3: Lead with the primary benefit or outcome | ||
""" | ||
} | ||
|
||
public static func makeGenerateExcerptPrompt( | ||
content: String, | ||
length: GeneratedContentLength, | ||
style: GenerationStyle | ||
) -> String { | ||
""" | ||
Generate excerpts with the following constraints (MUST FOLLOW): | ||
|
||
• Length: \(length.promptModifier) | ||
• Style: \(style.promptModifier) | ||
|
||
SOURCE POST CONTENT: | ||
\(content) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially wanted to trim HTML, but I wasn't sure it'll work well with Gutenberg blocks. Ideally, we want a rendered version of the post, but we don't have that. Based on my testing, it just figures it out, so I think we are good. |
||
""" | ||
} | ||
|
||
public static var generateMoreOptionsPrompt: String { | ||
"Generate additional three options" | ||
} | ||
} | ||
|
||
public enum GenerationStyle: String, CaseIterable, RawRepresentable { | ||
case engaging | ||
case conversational | ||
case witty | ||
case formal | ||
case professional | ||
|
||
public var displayName: String { | ||
switch self { | ||
case .engaging: | ||
NSLocalizedString("generation.style.engaging", value: "Engaging", comment: "AI generation style") | ||
case .conversational: | ||
NSLocalizedString("generation.style.conversational", value: "Conversational", comment: "AI generation style") | ||
case .witty: | ||
NSLocalizedString("generation.style.witty", value: "Witty", comment: "AI generation style") | ||
case .formal: | ||
NSLocalizedString("generation.style.formal", value: "Formal", comment: "AI generation style") | ||
case .professional: | ||
NSLocalizedString("generation.style.professional", value: "Professional", comment: "AI generation style") | ||
} | ||
} | ||
|
||
public var promptModifier: String { | ||
"\(rawValue) (\(promptModifierDetails))" | ||
} | ||
|
||
var promptModifierDetails: String { | ||
switch self { | ||
case .engaging: "engaging and compelling tone" | ||
case .witty: "witty, creative, entertaining" | ||
case .conversational: "friendly and conversational tone" | ||
case .formal: "formal and academic tone" | ||
case .professional: "professional and polished tone" | ||
} | ||
} | ||
} | ||
|
||
public enum GeneratedContentLength: Int, CaseIterable, RawRepresentable { | ||
case short | ||
case medium | ||
case long | ||
|
||
public var displayName: String { | ||
switch self { | ||
case .short: | ||
NSLocalizedString("generation.length.short", value: "Short", comment: "Generated content length (needs to be short)") | ||
case .medium: | ||
NSLocalizedString("generation.length.medium", value: "Medium", comment: "Generated content length (needs to be short)") | ||
case .long: | ||
NSLocalizedString("generation.length.long", value: "Long", comment: "Generated content length (needs to be short)") | ||
} | ||
} | ||
|
||
public var trackingName: String { name } | ||
|
||
public var promptModifier: String { | ||
"\(wordRange) words" | ||
} | ||
|
||
private var name: String { | ||
switch self { | ||
case .short: "short" | ||
case .medium: "medium" | ||
case .long: "long" | ||
} | ||
} | ||
|
||
private var wordRange: String { | ||
switch self { | ||
case .short: "20-40" | ||
case .medium: "50-70" | ||
case .long: "120-180" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import SwiftUI | ||
import FoundationModels | ||
|
||
@available(iOS 26, *) | ||
public struct LanguageModelUnavailableView: View { | ||
public let reason: SystemLanguageModel.Availability.UnavailableReason | ||
|
||
public var body: some View { | ||
makeUnavailableView(for: reason) | ||
} | ||
|
||
public init(reason: SystemLanguageModel.Availability.UnavailableReason) { | ||
self.reason = reason | ||
} | ||
|
||
@ViewBuilder | ||
private func makeUnavailableView(for reason: SystemLanguageModel.Availability.UnavailableReason) -> some View { | ||
switch reason { | ||
case .appleIntelligenceNotEnabled: | ||
EmptyStateView { | ||
Label(Strings.appleIntelligenceDisabledTitle, systemImage: "apple.intelligence") | ||
} description: { | ||
Text(Strings.appleIntelligenceDisabledMessage) | ||
} actions: { | ||
if let settingURL = URL(string: UIApplication.openSettingsURLString) { | ||
Button(Strings.openAppleIntelligenceSettings) { | ||
UIApplication.shared.open(settingURL) | ||
} | ||
.buttonStyle(.borderedProminent) | ||
.tint(AppColor.primary) | ||
} | ||
} | ||
case .modelNotReady: | ||
EmptyStateView { | ||
Label(Strings.preparingModel, systemImage: "apple.intelligence") | ||
} description: { | ||
Text(Strings.preparingModelDescription) | ||
} actions: { | ||
EmptyView() | ||
} | ||
default: | ||
EmptyStateView { | ||
Label(Strings.appleIntelligenceUnavailableTitle, systemImage: "apple.intelligence") | ||
} description: { | ||
Text(Strings.appleIntelligenceUnavailableTitle) | ||
} actions: { | ||
EmptyView() | ||
} | ||
} | ||
} | ||
} | ||
|
||
@available(iOS 26, *) | ||
#Preview { | ||
LanguageModelUnavailableView(reason: .appleIntelligenceNotEnabled) | ||
} | ||
|
||
private enum Strings { | ||
static let appleIntelligenceDisabledTitle = NSLocalizedString( | ||
"intelligence.unavailableView.appleIntelligenceDisabled.title", | ||
value: "Apple Intelligence Required", | ||
comment: "Title shown when Apple Intelligence is disabled" | ||
) | ||
|
||
static let appleIntelligenceDisabledMessage = NSLocalizedString( | ||
"intelligence.unavailableView.appleIntelligenceDisabled.message", | ||
value: "To generate excerpts with AI, please enable Apple Intelligence in Settings. This feature uses on-device processing to protect your privacy.", | ||
comment: "Message shown when Apple Intelligence is disabled" | ||
) | ||
|
||
static let openAppleIntelligenceSettings = NSLocalizedString( | ||
"intelligence.unavailableView.appleIntelligenceDisabled.openSettings", | ||
value: "Open Settings", | ||
comment: "Button to open Apple Intelligence settings" | ||
) | ||
|
||
static let preparingModel = NSLocalizedString( | ||
"intelligence.unavailableView.preparingModel.title", | ||
value: "Preparing model...", | ||
comment: "Title shown when the AI model is not ready" | ||
) | ||
|
||
static let preparingModelDescription = NSLocalizedString( | ||
"intelligence.unavailableView.preparingModel.description", | ||
value: "The AI model is downloading or being prepared. Please try again in a moment.", | ||
comment: "Description shown when the AI model is not ready" | ||
) | ||
|
||
static let appleIntelligenceUnavailableTitle = NSLocalizedString( | ||
"intelligence.unavailableView.appleIntelligenceUnvailable.title", | ||
value: "Apple Intelligence Unvailable", | ||
comment: "Title shown when Apple Intelligence is unavailable" | ||
) | ||
|
||
static let appleIntelligenceUnavailableMessage = NSLocalizedString( | ||
"intelligence.unavailableView.appleIntelligenceUnvailable.message", | ||
value: "Apple Intelligence is not available on this device", | ||
comment: "Message shown when Apple Intelligence is unavailable" | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
26.4 | ||
----- | ||
* [*] [Intelligence] Add support for generating excerpts for posts [#24852] | ||
|
||
26.3.1 | ||
------ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import SwiftUI | ||
|
||
extension Button where Label == Text { | ||
@ViewBuilder | ||
public static func make(role: BackportButtonRole, _ action: @escaping () -> Void) -> some View { | ||
if #available(iOS 26, *) { | ||
SwiftUI.Button(role: ButtonRole(role), action: action) | ||
} else { | ||
SwiftUI.Button(role.title) { | ||
action() | ||
} | ||
} | ||
} | ||
} | ||
|
||
public enum BackportButtonRole { | ||
case cancel | ||
case close | ||
case confirm | ||
|
||
var title: String { | ||
switch self { | ||
case .cancel: SharedStrings.Button.cancel | ||
case .close: SharedStrings.Button.close | ||
case .confirm: SharedStrings.Button.done | ||
} | ||
} | ||
} | ||
|
||
@available(iOS 26, *) | ||
private extension ButtonRole { | ||
init(_ role: BackportButtonRole) { | ||
switch role { | ||
case .cancel: self = .cancel | ||
case .close: self = .close | ||
case .confirm: self = .confirm | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generated result is English when the
content
is non-English. Adding a requirement here saying that the excerpt should be in the same language as the source content?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't going to test different locales but ran out of time.
If the post content is not supported, it will show this:
I can't get it to produce the results in anything other than English. I opened a ticket –https://linear.app/a8c/issue/CMM-762/excerpts-are-created-in-the-system-language-not-the-content-language. Help is welcome.