-
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
base: trunk
Are you sure you want to change the base?
Changes from 7 commits
f955c5d
459b548
405909b
560c62f
e568066
a0a852c
6011562
d740fbd
bc1b7a9
fd252ad
67a8e07
dec2b53
24ab67c
1c643f0
f5ed66b
7918244
821d52b
c8c149f
5beb232
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,128 @@ | ||
import Foundation | ||
|
||
#if canImport(FoundationModels) | ||
import FoundationModels | ||
#endif | ||
|
||
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 func makeGenerateExcerptPrompt( | ||
content: String, | ||
length: GeneratedContentLength, | ||
style: GenerationStyle | ||
) -> String { | ||
""" | ||
Task: Create exactly 3 excerpts for a blog post. | ||
|
||
CRITICAL CONSTRAINTS: | ||
• Each excerpt MUST be \(length.promptModifier) (\(length.wordRange) words) | ||
• Style: \(style.promptDescription) | ||
|
||
EXCERPT REQUIREMENTS: | ||
* Follow the best practices for post excerpts esteblished in the WordPress ecosystem | ||
• First sentence: Strong hook that creates curiosity | ||
• 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 | ||
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 generated result is English when 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 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. |
||
|
||
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 | ||
|
||
SOURCE 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 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 promptDescription: 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 { | ||
switch self { | ||
case .short: "short" | ||
case .medium: "medium" | ||
case .long: "long" | ||
} | ||
} | ||
|
||
public var promptModifier: String { | ||
switch self { | ||
case .short: "short" | ||
case .medium: "medium" | ||
case .long: "long" | ||
} | ||
} | ||
|
||
public var wordRange: String { | ||
switch self { | ||
case .short: "30-60" | ||
case .medium: "60-90" | ||
case .long: "120-150" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import SwiftUI | ||
|
||
#if canImport(FoundationModels) | ||
import FoundationModels | ||
#endif | ||
|
||
@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,6 +1,6 @@ | ||
26.4 | ||
----- | ||
|
||
* [*] [Intelligence] Add support for generating excerpts for posts [#24852] | ||
|
||
26.3 | ||
----- | ||
|
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
canImport
checks can be removed.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.
Claude added it for some reason and I kept it – removed.