浏览代码

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 2 月之前
父节点
当前提交
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 {
     private var selectedSidebarIndex: Int = 0
     /// 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.
     private var lastSearchResults: [JobListing] = []
+    /// "Show more jobs" row under the latest assistant message that listed jobs; removed when a newer listing block replaces it.
+    private var trailingLoadMoreJobsRow: NSView?
+    private weak var trailingLoadMoreJobsButton: HoverableButton?
     /// Most recently saved jobs appear first; persisted across launches.
     private var savedJobOrder: [JobListing] = []
     private var chatMessages: [ChatMessage] = []
     private var isAwaitingResponse = false
     private let jobSearchService = OpenAIJobSearchService()
 
+    /// Upper bound sent to the model per request (was fixed at 8 in the prompt). Clamped when calling the API.
+    private static let jobsPerSearchDefault = 15
+    private static let jobsPerSearchMin = 1
+    private static let jobsPerSearchMaxCap = 25
+
+    private static func clampedJobsPerRequest(_ requested: Int = jobsPerSearchDefault) -> Int {
+        min(jobsPerSearchMaxCap, max(jobsPerSearchMin, requested))
+    }
+
     private static let chatStatusSparklePulseKey = "chatStatusSparklePulse"
     private static let welcomeSubtitleBreathKey = "welcomeSubtitleBreath"
 
@@ -1308,11 +1320,27 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         appendChatBubble(text: prompt, isUser: true)
         chatMessages.append(ChatMessage(role: "user", content: prompt))
         jobKeywordsField.stringValue = ""
+        startJobSearchRequest(effectiveQuery: effectiveQuery, isContinuation: isContinuation)
+        window?.makeFirstResponder(nil)
+    }
+
+    @objc private func didTapLoadMoreJobs() {
+        let prompt = "Show more jobs"
+        guard !isAwaitingResponse, isContinuationPrompt(prompt) else { return }
+        if anchorUserJobQuery(excludingLatestUserMessage: prompt) == nil { return }
+        appendChatBubble(text: prompt, isUser: true)
+        chatMessages.append(ChatMessage(role: "user", content: prompt))
+        let effectiveQuery = resolvedSearchQuery(for: prompt)
+        startJobSearchRequest(effectiveQuery: effectiveQuery, isContinuation: true)
+    }
+
+    private func startJobSearchRequest(effectiveQuery: String, isContinuation: Bool) {
         isAwaitingResponse = true
         setChatStatusLabel("Thinking...")
         setInputEnabled(false)
         let contextMessages = chatMessages
-        jobSearchService.searchJobs(query: effectiveQuery, conversation: contextMessages) { [weak self] result in
+        let maxJobs = Self.clampedJobsPerRequest()
+        jobSearchService.searchJobs(query: effectiveQuery, conversation: contextMessages, maxJobs: maxJobs) { [weak self] result in
             DispatchQueue.main.async {
                 guard let self else { return }
                 self.isAwaitingResponse = false
@@ -1347,7 +1375,6 @@ final class DashboardView: NSView, NSTextFieldDelegate {
                 }
             }
         }
-        window?.makeFirstResponder(nil)
     }
 
     private func normalizedJobs(_ jobs: [JobListing]) -> [JobListing] {
@@ -1475,6 +1502,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
     }
 
     private func resetChatState() {
+        trailingLoadMoreJobsRow = nil
+        trailingLoadMoreJobsButton = nil
         chatMessages.removeAll()
         lastSearchResults.removeAll()
         chatStack.arrangedSubviews.forEach {
@@ -1550,9 +1579,16 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         column.translatesAutoresizingMaskIntoConstraints = false
 
         if let jobs, !jobs.isEmpty {
+            trailingLoadMoreJobsRow?.removeFromSuperview()
+            trailingLoadMoreJobsRow = nil
+            trailingLoadMoreJobsButton = nil
             let jobsStack = makeChatJobsStackView(jobs: jobs)
             column.addArrangedSubview(jobsStack)
             jobsStack.widthAnchor.constraint(equalTo: column.widthAnchor).isActive = true
+            let moreRow = makeLoadMoreJobsRowView()
+            column.addArrangedSubview(moreRow)
+            moreRow.widthAnchor.constraint(equalTo: column.widthAnchor).isActive = true
+            trailingLoadMoreJobsRow = moreRow
         }
 
         host.addSubview(avatar)
@@ -1633,6 +1669,29 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         return view
     }
 
+    private func makeLoadMoreJobsRowView() -> NSView {
+        let row = NSView()
+        row.translatesAutoresizingMaskIntoConstraints = false
+        let button = HoverableButton()
+        button.pointerCursor = true
+        button.title = "Show more jobs"
+        button.font = .systemFont(ofSize: 12, weight: .semibold)
+        button.bezelStyle = .rounded
+        button.controlSize = .regular
+        button.contentTintColor = Theme.brandBlue
+        button.target = self
+        button.action = #selector(didTapLoadMoreJobs)
+        button.translatesAutoresizingMaskIntoConstraints = false
+        trailingLoadMoreJobsButton = button
+        row.addSubview(button)
+        NSLayoutConstraint.activate([
+            button.leadingAnchor.constraint(equalTo: row.leadingAnchor),
+            button.topAnchor.constraint(equalTo: row.topAnchor, constant: 2),
+            button.bottomAnchor.constraint(equalTo: row.bottomAnchor, constant: -2)
+        ])
+        return row
+    }
+
     private func makeChatJobsStackView(jobs: [JobListing]) -> ChatJobsStackView {
         let stack = ChatJobsStackView()
         stack.orientation = .vertical
@@ -1659,6 +1718,8 @@ final class DashboardView: NSView, NSTextFieldDelegate {
         jobKeywordsField.isEnabled = enabled
         findJobsButton.isEnabled = enabled
         findJobsButton.alphaValue = enabled ? 1 : 0.65
+        trailingLoadMoreJobsButton?.isEnabled = enabled
+        trailingLoadMoreJobsButton?.alphaValue = enabled ? 1 : 0.65
     }
 
     private func configureSidebar() {
@@ -1885,7 +1946,8 @@ private final class OpenAIJobSearchService {
     private let endpoint = URL(string: "https://api.openai.com/v1/responses")!
     private let session = URLSession(configuration: .ephemeral)
 
-    func searchJobs(query: String, conversation: [ChatMessage], completion: @escaping (Result<JobSearchOutput, Error>) -> Void) {
+    func searchJobs(query: String, conversation: [ChatMessage], maxJobs: Int, completion: @escaping (Result<JobSearchOutput, Error>) -> Void) {
+        let jobLimit = max(1, min(maxJobs, 25))
         let apiKey = OpenAIConfiguration.apiKey
         guard OpenAIConfiguration.hasAPIKey else {
             completion(.failure(NSError(
@@ -1927,7 +1989,8 @@ private final class OpenAIJobSearchService {
         - The JSON must match exactly this shape (lowercase key "jobs"):
         {"jobs":[{"title":"...","description":"...","url":"https://..."}]}
         - If you find no suitable listings, return {"jobs":[]} — still valid JSON only.
-        Keep each description to one sentence; prefer real listing URLs; at most 8 jobs.
+        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.
+        Keep each description to one sentence; prefer real listing URLs.
         """
         let payload = OpenAIResponsesRequest(
             model: "gpt-4o-mini",