|
|
@@ -259,6 +259,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
mainOverlay.addArrangedSubview(chatStatusStack)
|
|
|
mainOverlay.addArrangedSubview(listingsTopSpacer)
|
|
|
mainOverlay.addArrangedSubview(chatScrollView)
|
|
|
+ mainOverlay.addArrangedSubview(jobListingsScrollView)
|
|
|
mainOverlay.addArrangedSubview(searchBarShadowHost)
|
|
|
|
|
|
contentStack.addArrangedSubview(sidebar)
|
|
|
@@ -291,10 +292,12 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
chatStatusStack.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
+ jobListingsScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
|
|
|
jobListingsContainer.topAnchor.constraint(equalTo: jobListingsScrollView.contentView.topAnchor),
|
|
|
jobListingsContainer.leadingAnchor.constraint(equalTo: jobListingsScrollView.contentView.leadingAnchor),
|
|
|
jobListingsContainer.widthAnchor.constraint(equalTo: jobListingsScrollView.contentView.widthAnchor),
|
|
|
+ jobListingsScrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200),
|
|
|
|
|
|
greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
|
|
|
greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -16),
|
|
|
@@ -1209,7 +1212,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
case .success(let output):
|
|
|
let normalizedJobs = self.normalizedJobs(output.jobs)
|
|
|
self.configureJobListings(normalizedJobs, noResultsForQuery: prompt)
|
|
|
- let reply = self.makeAssistantSearchReply(query: prompt, jobs: normalizedJobs, jsonResult: output.rawJSON)
|
|
|
+ let reply = self.makeAssistantSearchReply(query: prompt, jobs: normalizedJobs)
|
|
|
self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
|
|
|
self.appendChatBubble(text: reply, isUser: false)
|
|
|
self.chatStatusLabel.stringValue = "Ask for another job, company, or skill match"
|
|
|
@@ -1234,42 +1237,16 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
return trimmed.filter { !$0.title.isEmpty && !$0.description.isEmpty }
|
|
|
}
|
|
|
|
|
|
- private func makeAssistantSearchReply(query: String, jobs: [JobListing], jsonResult: String) -> String {
|
|
|
+ private func makeAssistantSearchReply(query: String, jobs: [JobListing]) -> String {
|
|
|
if jobs.isEmpty {
|
|
|
- return """
|
|
|
- No jobs were found for "\(query)".
|
|
|
-
|
|
|
- JSON result:
|
|
|
- \(jsonResult)
|
|
|
- """
|
|
|
- }
|
|
|
- let rows = jobs.prefix(8).enumerated().map { index, job in
|
|
|
- let fallback = "https://www.indeed.com/jobs?q=\(job.title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")"
|
|
|
- let link = (job.url?.isEmpty == false) ? job.url! : fallback
|
|
|
- let compactDescription = compactSingleLine(job.description, maxCharacters: 110)
|
|
|
- return "\(index + 1)) \(job.title) | \(compactDescription) | \(link)"
|
|
|
+ return "No jobs were found for \"\(query)\". Try another title, skill, company, or location."
|
|
|
}
|
|
|
return """
|
|
|
Found \(jobs.count) job result(s) for "\(query)".
|
|
|
-
|
|
|
- Row-wise results:
|
|
|
- \(rows.joined(separator: "\n"))
|
|
|
-
|
|
|
- JSON result:
|
|
|
- \(jsonResult)
|
|
|
+ Each result is shown as a card with the role, job description, and an Apply button that opens the job link.
|
|
|
"""
|
|
|
}
|
|
|
|
|
|
- private func compactSingleLine(_ text: String, maxCharacters: Int) -> String {
|
|
|
- let single = text
|
|
|
- .replacingOccurrences(of: "\n", with: " ")
|
|
|
- .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
|
|
|
- .trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
- guard single.count > maxCharacters, maxCharacters > 1 else { return single }
|
|
|
- let end = single.index(single.startIndex, offsetBy: maxCharacters - 1)
|
|
|
- return String(single[..<end]) + "…"
|
|
|
- }
|
|
|
-
|
|
|
func controlTextDidBeginEditing(_ obj: Notification) {
|
|
|
applySearchFieldInsertionPoint(obj.object)
|
|
|
if (obj.object as? NSTextField) === jobKeywordsField {
|
|
|
@@ -1665,7 +1642,7 @@ private final class OpenAIJobSearchService {
|
|
|
let cleanedText = Self.extractJSONObject(from: text)
|
|
|
let jsonData = Data(cleanedText.utf8)
|
|
|
let jobsPayload = try JSONDecoder().decode(JobSearchResultsPayload.self, from: jsonData)
|
|
|
- completion(.success(JobSearchOutput(jobs: jobsPayload.jobs, rawJSON: cleanedText)))
|
|
|
+ completion(.success(JobSearchOutput(jobs: jobsPayload.jobs)))
|
|
|
} catch {
|
|
|
let rawBody = String(data: data, encoding: .utf8) ?? "<non-utf8 response>"
|
|
|
let message = "The API response could not be parsed as job JSON. Raw response: \(rawBody.prefix(600))"
|
|
|
@@ -1772,7 +1749,6 @@ private struct JobSearchResultsPayload: Codable {
|
|
|
|
|
|
private struct JobSearchOutput {
|
|
|
let jobs: [JobListing]
|
|
|
- let rawJSON: String
|
|
|
}
|
|
|
|
|
|
private struct OpenAIAPIErrorResponse: Codable {
|