Skip to content

Commit 31362a1

Browse files
committed
Implement CMM-739: Generate post excerpt
1 parent bf25009 commit 31362a1

File tree

9 files changed

+1028
-97
lines changed

9 files changed

+1028
-97
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import Foundation
2+
3+
#if canImport(FoundationModels)
4+
import FoundationModels
5+
#endif
6+
7+
public enum LanguageModelHelper {
8+
public static var isSupported: Bool {
9+
guard #available(iOS 26, *) else { return false }
10+
switch SystemLanguageModel.default.availability {
11+
case .available:
12+
return true
13+
case .unavailable(let reason):
14+
switch reason {
15+
case .appleIntelligenceNotEnabled, .modelNotReady:
16+
return true
17+
case .deviceNotEligible:
18+
return false
19+
@unknown default:
20+
return false
21+
}
22+
}
23+
}
24+
25+
public static func makeGenerateExcerptPrompt(
26+
content: String,
27+
length: GeneratedContentLength,
28+
style: GenerationStyle
29+
) -> String {
30+
"""
31+
Generate three distinct excerpt options for the following post. Each excerpt should capture the essence of the content while being engaging enough to encourage readers to click through and read the full post.
32+
33+
**Parameters:**
34+
- Excerpt Length: around \(length.promptModifier) (important!!!)
35+
- Writing Style: \(style.promptDescription)
36+
37+
**Requirements for each excerpt:**
38+
39+
1. Stay within the target word count
40+
2. Hook the reader within the first sentence
41+
3. Maintain the specified writing style throughout
42+
4. Include the most compelling point or benefit from the post
43+
5. Use active voice predominantly
44+
6. Follow other best practices for post excerpts established within the WordPress community
45+
46+
**Generate three variations that differ in:**
47+
48+
- Opening approach (question vs. statement vs. statistic/fact)
49+
- Emotional appeal (logical vs. emotional vs. aspirational)
50+
- Information density (high detail vs. broad strokes vs. balanced)
51+
52+
**Post Content:**
53+
\(content)
54+
"""
55+
}
56+
}
57+
58+
public enum GenerationStyle: String, CaseIterable, RawRepresentable {
59+
case engaging
60+
case conversational
61+
case witty
62+
case formal
63+
case professional
64+
65+
public var displayName: String {
66+
switch self {
67+
case .engaging:
68+
NSLocalizedString("generation.style.engaging", value: "Engaging", comment: "AI generation style")
69+
case .conversational:
70+
NSLocalizedString("generation.style.conversational", value: "Conversational", comment: "AI generation style")
71+
case .witty:
72+
NSLocalizedString("generation.style.witty", value: "Witty", comment: "AI generation style")
73+
case .formal:
74+
NSLocalizedString("generation.style.formal", value: "Formal", comment: "AI generation style")
75+
case .professional:
76+
NSLocalizedString("generation.style.professional", value: "Professional", comment: "AI generation style")
77+
}
78+
}
79+
80+
public var promptDescription: String {
81+
switch self {
82+
case .engaging: "engaging and compelling tone"
83+
case .witty: "witty, creative, entertaining"
84+
case .conversational: "friendly and conversational tone"
85+
case .formal: "formal and academic tone"
86+
case .professional: "professional and polished tone"
87+
}
88+
}
89+
}
90+
91+
public enum GeneratedContentLength: Int, CaseIterable, RawRepresentable {
92+
case short
93+
case medium
94+
case long
95+
96+
public var displayName: String {
97+
switch self {
98+
case .short:
99+
NSLocalizedString("generation.length.short", value: "Short", comment: "Generated content length (needs to be short)")
100+
case .medium:
101+
NSLocalizedString("generation.length.medium", value: "Medium", comment: "Generated content length (needs to be short)")
102+
case .long:
103+
NSLocalizedString("generation.length.long", value: "Long", comment: "Generated content length (needs to be short)")
104+
}
105+
}
106+
107+
public var promptModifier: String {
108+
switch self {
109+
case .short: "50 words"
110+
case .medium: "100 words"
111+
case .long: "150 words"
112+
}
113+
}
114+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import SwiftUI
2+
3+
#if canImport(FoundationModels)
4+
import FoundationModels
5+
#endif
6+
7+
@available(iOS 26, *)
8+
public struct LanguageModelUnavailableView: View {
9+
public let reason: SystemLanguageModel.Availability.UnavailableReason
10+
11+
public var body: some View {
12+
makeUnavailableView(for: reason)
13+
}
14+
15+
public init(reason: SystemLanguageModel.Availability.UnavailableReason) {
16+
self.reason = reason
17+
}
18+
19+
@ViewBuilder
20+
private func makeUnavailableView(for reason: SystemLanguageModel.Availability.UnavailableReason) -> some View {
21+
switch reason {
22+
case .appleIntelligenceNotEnabled:
23+
EmptyStateView {
24+
Label(Strings.appleIntelligenceDisabledTitle, systemImage: "apple.intelligence")
25+
} description: {
26+
Text(Strings.appleIntelligenceDisabledMessage)
27+
} actions: {
28+
VStack(spacing: 12) {
29+
Button(Strings.openAppleIntelligenceSettings) {
30+
openAppleIntelligenceSettings()
31+
}
32+
.buttonStyle(.borderedProminent)
33+
.tint(AppColor.primary)
34+
}
35+
}
36+
case .modelNotReady:
37+
EmptyStateView {
38+
Label(Strings.preparingModel, systemImage: "apple.intelligence")
39+
} description: {
40+
Text(Strings.preparingModelDescription)
41+
} actions: {
42+
EmptyView()
43+
}
44+
default:
45+
EmptyStateView {
46+
Label(Strings.appleIntelligenceUnavailableTitle, systemImage: "apple.intelligence")
47+
} description: {
48+
Text(Strings.appleIntelligenceUnavailableTitle)
49+
} actions: {
50+
EmptyView()
51+
}
52+
}
53+
}
54+
}
55+
56+
private func openAppleIntelligenceSettings() {
57+
if let settingsURL = URL(string: "App-Prefs:root=APPLE_INTELLIGENCE_SIRI_SETTINGS") {
58+
UIApplication.shared.open(settingsURL) { success in
59+
if !success {
60+
// Fallback to general Settings app
61+
if let generalSettingsURL = URL(string: UIApplication.openSettingsURLString) {
62+
UIApplication.shared.open(generalSettingsURL)
63+
}
64+
}
65+
}
66+
}
67+
}
68+
69+
@available(iOS 26, *)
70+
#Preview {
71+
LanguageModelUnavailableView(reason: .appleIntelligenceNotEnabled)
72+
}
73+
74+
private enum Strings {
75+
static let appleIntelligenceDisabledTitle = NSLocalizedString(
76+
"onDeviceAI.unavailableView.appleIntelligenceDisabled.title",
77+
value: "Apple Intelligence Required",
78+
comment: "Title shown when Apple Intelligence is disabled"
79+
)
80+
81+
static let appleIntelligenceDisabledMessage = NSLocalizedString(
82+
"onDeviceAI.unavailableView.appleIntelligenceDisabled.message",
83+
value: "To generate excerpts with AI, please enable Apple Intelligence in Settings. This feature uses on-device processing to protect your privacy.",
84+
comment: "Message shown when Apple Intelligence is disabled"
85+
)
86+
87+
static let openAppleIntelligenceSettings = NSLocalizedString(
88+
"onDeviceAI.unavailableView.appleIntelligenceDisabled.openSettings",
89+
value: "Open Settings",
90+
comment: "Button to open Apple Intelligence settings"
91+
)
92+
93+
static let preparingModel = NSLocalizedString(
94+
"onDeviceAI.unavailableView.preparingModel.title",
95+
value: "Preparing model...",
96+
comment: "Title shown when the AI model is not ready"
97+
)
98+
99+
static let preparingModelDescription = NSLocalizedString(
100+
"onDeviceAI.unavailableView.preparingModel.description",
101+
value: "The AI model is downloading or being prepared. Please try again in a moment.",
102+
comment: "Description shown when the AI model is not ready"
103+
)
104+
105+
static let appleIntelligenceUnavailableTitle = NSLocalizedString(
106+
"onDeviceAI.unavailableView.appleIntelligenceUnvailable.title",
107+
value: "Apple Intelligence Unvailable",
108+
comment: "Title shown when Apple Intelligence is unavailable"
109+
)
110+
111+
static let appleIntelligenceUnavailableMessage = NSLocalizedString(
112+
"onDeviceAI.unavailableView.appleIntelligenceUnvailable.message",
113+
value: "Apple Intelligence is not available on this device",
114+
comment: "Message shown when Apple Intelligence is unavailable"
115+
)
116+
}

RELEASE-NOTES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
26.4
22
-----
3-
3+
* [*] [Intelligence] Add support for generating excerpts for posts [#24852]
44

55
26.3
66
-----
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import SwiftUI
2+
3+
extension Button where Label == Text {
4+
@ViewBuilder
5+
public static func make(role: BackportButtonRole, _ action: @escaping () -> Void) -> some View {
6+
if #available(iOS 26, *) {
7+
SwiftUI.Button(role: ButtonRole(role), action: action)
8+
} else {
9+
SwiftUI.Button(role.title) {
10+
action()
11+
}
12+
}
13+
}
14+
}
15+
16+
public enum BackportButtonRole {
17+
case cancel
18+
case close
19+
case confirm
20+
21+
var title: String {
22+
switch self {
23+
case .cancel: SharedStrings.Button.cancel
24+
case .close: SharedStrings.Button.close
25+
case .confirm: SharedStrings.Button.done
26+
}
27+
}
28+
}
29+
30+
@available(iOS 26, *)
31+
private extension ButtonRole {
32+
init(_ role: BackportButtonRole) {
33+
switch role {
34+
case .cancel: self = .cancel
35+
case .close: self = .close
36+
case .confirm: self = .confirm
37+
}
38+
}
39+
}

WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,11 @@ private struct PostSettingsView: View {
182182
private var excerptSection: some View {
183183
Section {
184184
NavigationLink {
185-
PostSettingsExcerptEditor(text: $viewModel.settings.excerpt)
186-
.navigationTitle(Strings.excerptHeader)
185+
PostSettingsExcerptEditor(
186+
postContent: (viewModel.post.content ?? ""),
187+
text: $viewModel.settings.excerpt
188+
)
189+
.navigationTitle(Strings.excerptHeader)
187190
} label: {
188191
PostSettingExcerptRow(text: viewModel.settings.excerpt)
189192
}

0 commit comments

Comments
 (0)