|
|
@@ -19,6 +19,26 @@ private enum PremiumSheetLayout {
|
|
19
|
19
|
static let overscanExtraTop: CGFloat = 0.5
|
|
20
|
20
|
}
|
|
21
|
21
|
|
|
|
22
|
+/// Free-tier cap for Home AI job search (user messages only; Pro is unlimited).
|
|
|
23
|
+private enum FreeTierJobSearchQuota {
|
|
|
24
|
+ static let maxUserMessages = 2
|
|
|
25
|
+ private static let userDefaultsKey = "com.appforindeed.freeJobSearchUserMessageCount"
|
|
|
26
|
+
|
|
|
27
|
+ static var userMessageCount: Int {
|
|
|
28
|
+ get { UserDefaults.standard.integer(forKey: userDefaultsKey) }
|
|
|
29
|
+ set { UserDefaults.standard.set(newValue, forKey: userDefaultsKey) }
|
|
|
30
|
+ }
|
|
|
31
|
+
|
|
|
32
|
+ static func canSendAnotherMessage(isProActive: Bool) -> Bool {
|
|
|
33
|
+ isProActive || userMessageCount < maxUserMessages
|
|
|
34
|
+ }
|
|
|
35
|
+
|
|
|
36
|
+ static func recordUserMessageSent(isProActive: Bool) {
|
|
|
37
|
+ guard !isProActive else { return }
|
|
|
38
|
+ userMessageCount += 1
|
|
|
39
|
+ }
|
|
|
40
|
+}
|
|
|
41
|
+
|
|
22
|
42
|
private enum SettingsAppearanceID {
|
|
23
|
43
|
static let section = "dashboard.settings.section"
|
|
24
|
44
|
static let sectionHeader = "dashboard.settings.sectionHeader"
|
|
|
@@ -597,6 +617,17 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
597
|
617
|
return true
|
|
598
|
618
|
}
|
|
599
|
619
|
|
|
|
620
|
+ /// Home AI job search: Pro is unlimited; free users may send up to `FreeTierJobSearchQuota.maxUserMessages` user messages.
|
|
|
621
|
+ @discardableResult
|
|
|
622
|
+ private func ensureProAccessForJobSearch() -> Bool {
|
|
|
623
|
+ if SubscriptionStore.shared.isProActive { return true }
|
|
|
624
|
+ guard FreeTierJobSearchQuota.canSendAnotherMessage(isProActive: false) else {
|
|
|
625
|
+ presentPremiumPlansSheet()
|
|
|
626
|
+ return false
|
|
|
627
|
+ }
|
|
|
628
|
+ return true
|
|
|
629
|
+ }
|
|
|
630
|
+
|
|
600
|
631
|
private func presentPremiumPlansSheet() {
|
|
601
|
632
|
guard let hostWindow = window else { return }
|
|
602
|
633
|
|
|
|
@@ -1998,7 +2029,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
1998
|
2029
|
}
|
|
1999
|
2030
|
|
|
2000
|
2031
|
@objc private func didSubmitSearch() {
|
|
2001
|
|
- guard ensureProAccess() else { return }
|
|
|
2032
|
+ guard ensureProAccessForJobSearch() else { return }
|
|
2002
|
2033
|
let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2003
|
2034
|
guard !prompt.isEmpty, !isAwaitingResponse else { return }
|
|
2004
|
2035
|
let isContinuation = isContinuationPrompt(prompt)
|
|
|
@@ -2116,7 +2147,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
2116
|
2147
|
}
|
|
2117
|
2148
|
|
|
2118
|
2149
|
private func focusSearchField(seed: String) {
|
|
2119
|
|
- guard ensureProAccess() else { return }
|
|
|
2150
|
+ guard ensureProAccessForJobSearch() else { return }
|
|
2120
|
2151
|
jobKeywordsField.stringValue = seed
|
|
2121
|
2152
|
window?.makeFirstResponder(jobKeywordsField)
|
|
2122
|
2153
|
if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
|
|
|
@@ -2125,7 +2156,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
2125
|
2156
|
}
|
|
2126
|
2157
|
|
|
2127
|
2158
|
@objc private func didTapLoadMoreJobs() {
|
|
2128
|
|
- guard ensureProAccess() else { return }
|
|
|
2159
|
+ guard ensureProAccessForJobSearch() else { return }
|
|
2129
|
2160
|
let prompt = "Show more jobs"
|
|
2130
|
2161
|
guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
|
|
2131
|
2162
|
if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
|
|
|
@@ -2136,6 +2167,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
2136
|
2167
|
}
|
|
2137
|
2168
|
|
|
2138
|
2169
|
private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
|
|
|
2170
|
+ FreeTierJobSearchQuota.recordUserMessageSent(isProActive: SubscriptionStore.shared.isProActive)
|
|
2139
|
2171
|
isAwaitingResponse = true
|
|
2140
|
2172
|
addInlineChatThinkingRow()
|
|
2141
|
2173
|
setInputEnabled(false)
|