|
@@ -19,6 +19,26 @@ private enum PremiumSheetLayout {
|
|
|
static let overscanExtraTop: CGFloat = 0.5
|
|
static let overscanExtraTop: CGFloat = 0.5
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// Free-tier cap for Home AI job search (user messages only; Pro is unlimited).
|
|
|
|
|
+private enum FreeTierJobSearchQuota {
|
|
|
|
|
+ static let maxUserMessages = 2
|
|
|
|
|
+ private static let userDefaultsKey = "com.appforindeed.freeJobSearchUserMessageCount"
|
|
|
|
|
+
|
|
|
|
|
+ static var userMessageCount: Int {
|
|
|
|
|
+ get { UserDefaults.standard.integer(forKey: userDefaultsKey) }
|
|
|
|
|
+ set { UserDefaults.standard.set(newValue, forKey: userDefaultsKey) }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static func canSendAnotherMessage(isProActive: Bool) -> Bool {
|
|
|
|
|
+ isProActive || userMessageCount < maxUserMessages
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static func recordUserMessageSent(isProActive: Bool) {
|
|
|
|
|
+ guard !isProActive else { return }
|
|
|
|
|
+ userMessageCount += 1
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
private enum SettingsAppearanceID {
|
|
private enum SettingsAppearanceID {
|
|
|
static let section = "dashboard.settings.section"
|
|
static let section = "dashboard.settings.section"
|
|
|
static let sectionHeader = "dashboard.settings.sectionHeader"
|
|
static let sectionHeader = "dashboard.settings.sectionHeader"
|
|
@@ -597,6 +617,17 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// Home AI job search: Pro is unlimited; free users may send up to `FreeTierJobSearchQuota.maxUserMessages` user messages.
|
|
|
|
|
+ @discardableResult
|
|
|
|
|
+ private func ensureProAccessForJobSearch() -> Bool {
|
|
|
|
|
+ if SubscriptionStore.shared.isProActive { return true }
|
|
|
|
|
+ guard FreeTierJobSearchQuota.canSendAnotherMessage(isProActive: false) else {
|
|
|
|
|
+ presentPremiumPlansSheet()
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func presentPremiumPlansSheet() {
|
|
private func presentPremiumPlansSheet() {
|
|
|
guard let hostWindow = window else { return }
|
|
guard let hostWindow = window else { return }
|
|
|
|
|
|
|
@@ -1998,7 +2029,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func didSubmitSearch() {
|
|
@objc private func didSubmitSearch() {
|
|
|
- guard ensureProAccess() else { return }
|
|
|
|
|
|
|
+ guard ensureProAccessForJobSearch() else { return }
|
|
|
let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let prompt = jobKeywordsField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
guard !prompt.isEmpty, !isAwaitingResponse else { return }
|
|
guard !prompt.isEmpty, !isAwaitingResponse else { return }
|
|
|
let isContinuation = isContinuationPrompt(prompt)
|
|
let isContinuation = isContinuationPrompt(prompt)
|
|
@@ -2116,7 +2147,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func focusSearchField(seed: String) {
|
|
private func focusSearchField(seed: String) {
|
|
|
- guard ensureProAccess() else { return }
|
|
|
|
|
|
|
+ guard ensureProAccessForJobSearch() else { return }
|
|
|
jobKeywordsField.stringValue = seed
|
|
jobKeywordsField.stringValue = seed
|
|
|
window?.makeFirstResponder(jobKeywordsField)
|
|
window?.makeFirstResponder(jobKeywordsField)
|
|
|
if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
|
|
if let editor = jobKeywordsField.window?.fieldEditor(true, for: jobKeywordsField) as? NSTextView {
|
|
@@ -2125,7 +2156,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func didTapLoadMoreJobs() {
|
|
@objc private func didTapLoadMoreJobs() {
|
|
|
- guard ensureProAccess() else { return }
|
|
|
|
|
|
|
+ guard ensureProAccessForJobSearch() else { return }
|
|
|
let prompt = "Show more jobs"
|
|
let prompt = "Show more jobs"
|
|
|
guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
|
|
guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
|
|
|
if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
|
|
if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
|
|
@@ -2136,6 +2167,7 @@ final class DashboardView: NSView, NSTextFieldDelegate, NSSharingServicePickerDe
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
|
|
private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
|
|
|
|
|
+ FreeTierJobSearchQuota.recordUserMessageSent(isProActive: SubscriptionStore.shared.isProActive)
|
|
|
isAwaitingResponse = true
|
|
isAwaitingResponse = true
|
|
|
addInlineChatThinkingRow()
|
|
addInlineChatThinkingRow()
|
|
|
setInputEnabled(false)
|
|
setInputEnabled(false)
|