Преглед изворни кода

Raise job search batch limit and add Show more jobs control.

Replace the hardcoded eight-job cap in the OpenAI prompt with a configurable
limit (default 15, max 25). Refactor search submission into a shared path and
add a Show more jobs button that triggers the same continuation flow as typed
more requests, with the button disabled while a search is in flight.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 пре 3 недеља
родитељ
комит
f1f52e9234
1 измењених фајлова са 67 додато и 4 уклоњено
  1. 67 4
      App for Indeed/Views/DashboardView.swift

+ 67 - 4
App for Indeed/Views/DashboardView.swift

@@ -107,12 +107,24 @@ final class DashboardView: NSView, NSTextFieldDelegate {
107
     private var selectedSidebarIndex: Int = 0
107
     private var selectedSidebarIndex: Int = 0
108
     /// All jobs that have been shown in the current chat session, oldest first. Used to deduplicate continuation searches so "show more" doesn't re-display the same listings.
108
     /// All jobs that have been shown in the current chat session, oldest first. Used to deduplicate continuation searches so "show more" doesn't re-display the same listings.
109
     private var lastSearchResults: [JobListing] = []
109
     private var lastSearchResults: [JobListing] = []
110
+    /// "Show more jobs" row under the latest assistant message that listed jobs; removed when a newer listing block replaces it.
111
+    private var trailingLoadMoreJobsRow: NSView?
112
+    private weak var trailingLoadMoreJobsButton: HoverableButton?
110
     /// Most recently saved jobs appear first; persisted across launches.
113
     /// Most recently saved jobs appear first; persisted across launches.
111
     private var savedJobOrder: [JobListing] = []
114
     private var savedJobOrder: [JobListing] = []
112
     private var chatMessages: [ChatMessage] = []
115
     private var chatMessages: [ChatMessage] = []
113
     private var isAwaitingResponse = false
116
     private var isAwaitingResponse = false
114
     private let jobSearchService = OpenAIJobSearchService()
117
     private let jobSearchService = OpenAIJobSearchService()
115
 
118
 
119
+    /// Upper bound sent to the model per request (was fixed at 8 in the prompt). Clamped when calling the API.
120
+    private static let jobsPerSearchDefault = 15
121
+    private static let jobsPerSearchMin = 1
122
+    private static let jobsPerSearchMaxCap = 25
123
+
124
+    private static func clampedJobsPerRequest(_ requested: Int = jobsPerSearchDefault) -> Int {
125
+        min(jobsPerSearchMaxCap, max(jobsPerSearchMin, requested))
126
+    }
127
+
116
     private static let chatStatusSparklePulseKey = "chatStatusSparklePulse"
128
     private static let chatStatusSparklePulseKey = "chatStatusSparklePulse"
117
     private static let welcomeSubtitleBreathKey = "welcomeSubtitleBreath"
129
     private static let welcomeSubtitleBreathKey = "welcomeSubtitleBreath"
118
 
130
 
@@ -1308,11 +1320,27 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1308
         appendChatBubble(text: prompt, isUser: true)
1320
         appendChatBubble(text: prompt, isUser: true)
1309
         chatMessages.append(ChatMessage(role: "user", content: prompt))
1321
         chatMessages.append(ChatMessage(role: "user", content: prompt))
1310
         jobKeywordsField.stringValue = ""
1322
         jobKeywordsField.stringValue = ""
1323
+        startJobSearchRequest(effectiveQuery: effectiveQuery, isContinuation: isContinuation)
1324
+        window?.makeFirstResponder(nil)
1325
+    }
1326
+
1327
+    @objc private func didTapLoadMoreJobs() {
1328
+        let prompt = "Show more jobs"
1329
+        guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
1330
+        if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
1331
+        appendChatBubble(text: prompt, isUser: true)
1332
+        chatMessages.append(ChatMessage(role: "user", content: prompt))
1333
+        let effectiveQuery = resolvedSearchQuery(for: prompt)
1334
+        startJobSearchRequest(effectiveQuery: effectiveQuery, isContinuation: true)
1335
+    }
1336
+
1337
+    private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
1311
         isAwaitingResponse = true
1338
         isAwaitingResponse = true
1312
         setChatStatusLabel("Thinking...")
1339
         setChatStatusLabel("Thinking...")
1313
         setInputEnabled(false)
1340
         setInputEnabled(false)
1314
         let contextMessages = chatMessages
1341
         let contextMessages = chatMessages
1315
-        jobSearchService.searchJobs(query: effectiveQuery, conversation: contextMessages) { [weak self] result in
1342
+        let maxJobs = Self.clampedJobsPerRequest()
1343
+        jobSearchService.searchJobs(query: effectiveQuery, conversation: contextMessages, maxJobs: maxJobs) { [weak self] result in
1316
             DispatchQueue.main.async {
1344
             DispatchQueue.main.async {
1317
                 guard let self else { return }
1345
                 guard let self else { return }
1318
                 self.isAwaitingResponse = false
1346
                 self.isAwaitingResponse = false
@@ -1347,7 +1375,6 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1347
                 }
1375
                 }
1348
             }
1376
             }
1349
         }
1377
         }
1350
-        window?.makeFirstResponder(nil)
1351
     }
1378
     }
1352
 
1379
 
1353
     private func normalizedJobs(_ jobs: [JobListing]) -> [JobListing] {
1380
     private func normalizedJobs(_ jobs: [JobListing]) -> [JobListing] {
@@ -1475,6 +1502,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1475
     }
1502
     }
1476
 
1503
 
1477
     private func resetChatState() {
1504
     private func resetChatState() {
1505
+        trailingLoadMoreJobsRow = nil
1506
+        trailingLoadMoreJobsButton = nil
1478
         chatMessages.removeAll()
1507
         chatMessages.removeAll()
1479
         lastSearchResults.removeAll()
1508
         lastSearchResults.removeAll()
1480
         chatStack.arrangedSubviews.forEach {
1509
         chatStack.arrangedSubviews.forEach {
@@ -1550,9 +1579,16 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1550
         column.translatesAutoresizingMaskIntoConstraints = false
1579
         column.translatesAutoresizingMaskIntoConstraints = false
1551
 
1580
 
1552
         if let jobs, !jobs.isEmpty {
1581
         if let jobs, !jobs.isEmpty {
1582
+            trailingLoadMoreJobsRow?.removeFromSuperview()
1583
+            trailingLoadMoreJobsRow = nil
1584
+            trailingLoadMoreJobsButton = nil
1553
             let jobsStack = makeChatJobsStackView(jobs: jobs)
1585
             let jobsStack = makeChatJobsStackView(jobs: jobs)
1554
             column.addArrangedSubview(jobsStack)
1586
             column.addArrangedSubview(jobsStack)
1555
             jobsStack.widthAnchor.constraint(equalTo: column.widthAnchor).isActive = true
1587
             jobsStack.widthAnchor.constraint(equalTo: column.widthAnchor).isActive = true
1588
+            let moreRow = makeLoadMoreJobsRowView()
1589
+            column.addArrangedSubview(moreRow)
1590
+            moreRow.widthAnchor.constraint(equalTo: column.widthAnchor).isActive = true
1591
+            trailingLoadMoreJobsRow = moreRow
1556
         }
1592
         }
1557
 
1593
 
1558
         host.addSubview(avatar)
1594
         host.addSubview(avatar)
@@ -1633,6 +1669,29 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1633
         return view
1669
         return view
1634
     }
1670
     }
1635
 
1671
 
1672
+    private func makeLoadMoreJobsRowView() -> NSView {
1673
+        let row = NSView()
1674
+        row.translatesAutoresizingMaskIntoConstraints = false
1675
+        let button = HoverableButton()
1676
+        button.pointerCursor = true
1677
+        button.title = "Show more jobs"
1678
+        button.font = .systemFont(ofSize: 12, weight: .semibold)
1679
+        button.bezelStyle = .rounded
1680
+        button.controlSize = .regular
1681
+        button.contentTintColor = Theme.brandBlue
1682
+        button.target = self
1683
+        button.action = #selector(didTapLoadMoreJobs)
1684
+        button.translatesAutoresizingMaskIntoConstraints = false
1685
+        trailingLoadMoreJobsButton = button
1686
+        row.addSubview(button)
1687
+        NSLayoutConstraint.activate([
1688
+            button.leadingAnchor.constraint(equalTo: row.leadingAnchor),
1689
+            button.topAnchor.constraint(equalTo: row.topAnchor, constant: 2),
1690
+            button.bottomAnchor.constraint(equalTo: row.bottomAnchor, constant: -2)
1691
+        ])
1692
+        return row
1693
+    }
1694
+
1636
     private func makeChatJobsStackView(jobs: [JobListing]) -> ChatJobsStackView {
1695
     private func makeChatJobsStackView(jobs: [JobListing]) -> ChatJobsStackView {
1637
         let stack = ChatJobsStackView()
1696
         let stack = ChatJobsStackView()
1638
         stack.orientation = .vertical
1697
         stack.orientation = .vertical
@@ -1659,6 +1718,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1659
         jobKeywordsField.isEnabled = enabled
1718
         jobKeywordsField.isEnabled = enabled
1660
         findJobsButton.isEnabled = enabled
1719
         findJobsButton.isEnabled = enabled
1661
         findJobsButton.alphaValue = enabled ? 1 : 0.65
1720
         findJobsButton.alphaValue = enabled ? 1 : 0.65
1721
+        trailingLoadMoreJobsButton?.isEnabled = enabled
1722
+        trailingLoadMoreJobsButton?.alphaValue = enabled ? 1 : 0.65
1662
     }
1723
     }
1663
 
1724
 
1664
     private func configureSidebar() {
1725
     private func configureSidebar() {
@@ -1885,7 +1946,8 @@ private final class OpenAIJobSearchService {
1885
     private let endpoint = URL(string: "https://api.openai.com/v1/responses")!
1946
     private let endpoint = URL(string: "https://api.openai.com/v1/responses")!
1886
     private let session = URLSession(configuration: .ephemeral)
1947
     private let session = URLSession(configuration: .ephemeral)
1887
 
1948
 
1888
-    func searchJobs(query: String, conversation: [ChatMessage], completion: @escaping (Result<JobSearchOutput, Error>) -> Void) {
1949
+    func searchJobs(query: String, conversation: [ChatMessage], maxJobs: Int, completion: @escaping (Result<JobSearchOutput, Error>) -> Void) {
1950
+        let jobLimit = max(1, min(maxJobs, 25))
1889
         let apiKey = OpenAIConfiguration.apiKey
1951
         let apiKey = OpenAIConfiguration.apiKey
1890
         guard OpenAIConfiguration.hasAPIKey else {
1952
         guard OpenAIConfiguration.hasAPIKey else {
1891
             completion(.failure(NSError(
1953
             completion(.failure(NSError(
@@ -1927,7 +1989,8 @@ private final class OpenAIJobSearchService {
1927
         - The JSON must match exactly this shape (lowercase key "jobs"):
1989
         - The JSON must match exactly this shape (lowercase key "jobs"):
1928
         {"jobs":[{"title":"...","description":"...","url":"https://..."}]}
1990
         {"jobs":[{"title":"...","description":"...","url":"https://..."}]}
1929
         - If you find no suitable listings, return {"jobs":[]} — still valid JSON only.
1991
         - If you find no suitable listings, return {"jobs":[]} — still valid JSON only.
1930
-        Keep each description to one sentence; prefer real listing URLs; at most 8 jobs.
1992
+        Return up to \(jobLimit) jobs in the jobs array (fewer only if the web has no additional distinct matches). Do not repeat titles or URLs already implied in the conversation context above.
1993
+        Keep each description to one sentence; prefer real listing URLs.
1931
         """
1994
         """
1932
         let payload = OpenAIResponsesRequest(
1995
         let payload = OpenAIResponsesRequest(
1933
             model: "gpt-4o-mini",
1996
             model: "gpt-4o-mini",