Pārlūkot izejas kodu

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 nedēļas atpakaļ
vecāks
revīzija
f1f52e9234
1 mainītis faili ar 67 papildinājumiem un 4 dzēšanām
  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 107
     private var selectedSidebarIndex: Int = 0
108 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 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 113
     /// Most recently saved jobs appear first; persisted across launches.
111 114
     private var savedJobOrder: [JobListing] = []
112 115
     private var chatMessages: [ChatMessage] = []
113 116
     private var isAwaitingResponse = false
114 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 128
     private static let chatStatusSparklePulseKey = "chatStatusSparklePulse"
117 129
     private static let welcomeSubtitleBreathKey = "welcomeSubtitleBreath"
118 130
 
@@ -1308,11 +1320,27 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1308 1320
         appendChatBubble(text: prompt, isUser: true)
1309 1321
         chatMessages.append(ChatMessage(role: "user", content: prompt))
1310 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 1338
         isAwaitingResponse = true
1312 1339
         setChatStatusLabel("Thinking...")
1313 1340
         setInputEnabled(false)
1314 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 1344
             DispatchQueue.main.async {
1317 1345
                 guard let self else { return }
1318 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 1380
     private func normalizedJobs(_ jobs: [JobListing]) -> [JobListing] {
@@ -1475,6 +1502,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1475 1502
     }
1476 1503
 
1477 1504
     private func resetChatState() {
1505
+        trailingLoadMoreJobsRow = nil
1506
+        trailingLoadMoreJobsButton = nil
1478 1507
         chatMessages.removeAll()
1479 1508
         lastSearchResults.removeAll()
1480 1509
         chatStack.arrangedSubviews.forEach {
@@ -1550,9 +1579,16 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1550 1579
         column.translatesAutoresizingMaskIntoConstraints = false
1551 1580
 
1552 1581
         if let jobs, !jobs.isEmpty {
1582
+            trailingLoadMoreJobsRow?.removeFromSuperview()
1583
+            trailingLoadMoreJobsRow = nil
1584
+            trailingLoadMoreJobsButton = nil
1553 1585
             let jobsStack = makeChatJobsStackView(jobs: jobs)
1554 1586
             column.addArrangedSubview(jobsStack)
1555 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 1594
         host.addSubview(avatar)
@@ -1633,6 +1669,29 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1633 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 1695
     private func makeChatJobsStackView(jobs: [JobListing]) -> ChatJobsStackView {
1637 1696
         let stack = ChatJobsStackView()
1638 1697
         stack.orientation = .vertical
@@ -1659,6 +1718,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
1659 1718
         jobKeywordsField.isEnabled = enabled
1660 1719
         findJobsButton.isEnabled = enabled
1661 1720
         findJobsButton.alphaValue = enabled ? 1 : 0.65
1721
+        trailingLoadMoreJobsButton?.isEnabled = enabled
1722
+        trailingLoadMoreJobsButton?.alphaValue = enabled ? 1 : 0.65
1662 1723
     }
1663 1724
 
1664 1725
     private func configureSidebar() {
@@ -1885,7 +1946,8 @@ private final class OpenAIJobSearchService {
1885 1946
     private let endpoint = URL(string: "https://api.openai.com/v1/responses")!
1886 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 1951
         let apiKey = OpenAIConfiguration.apiKey
1890 1952
         guard OpenAIConfiguration.hasAPIKey else {
1891 1953
             completion(.failure(NSError(
@@ -1927,7 +1989,8 @@ private final class OpenAIJobSearchService {
1927 1989
         - The JSON must match exactly this shape (lowercase key "jobs"):
1928 1990
         {"jobs":[{"title":"...","description":"...","url":"https://..."}]}
1929 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 1995
         let payload = OpenAIResponsesRequest(
1933 1996
             model: "gpt-4o-mini",