|
@@ -259,6 +259,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
259
|
mainOverlay.addArrangedSubview(chatStatusStack)
|
259
|
mainOverlay.addArrangedSubview(chatStatusStack)
|
|
260
|
mainOverlay.addArrangedSubview(listingsTopSpacer)
|
260
|
mainOverlay.addArrangedSubview(listingsTopSpacer)
|
|
261
|
mainOverlay.addArrangedSubview(chatScrollView)
|
261
|
mainOverlay.addArrangedSubview(chatScrollView)
|
|
|
|
262
|
+ mainOverlay.addArrangedSubview(jobListingsScrollView)
|
|
262
|
mainOverlay.addArrangedSubview(searchBarShadowHost)
|
263
|
mainOverlay.addArrangedSubview(searchBarShadowHost)
|
|
263
|
|
264
|
|
|
264
|
contentStack.addArrangedSubview(sidebar)
|
265
|
contentStack.addArrangedSubview(sidebar)
|
|
@@ -291,10 +292,12 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
291
|
searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
292
|
searchBarShadowHost.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
292
|
chatStatusStack.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
293
|
chatStatusStack.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
293
|
chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
294
|
chatScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
|
|
295
|
+ jobListingsScrollView.widthAnchor.constraint(equalTo: mainOverlay.widthAnchor, multiplier: 0.92),
|
|
294
|
|
296
|
|
|
295
|
jobListingsContainer.topAnchor.constraint(equalTo: jobListingsScrollView.contentView.topAnchor),
|
297
|
jobListingsContainer.topAnchor.constraint(equalTo: jobListingsScrollView.contentView.topAnchor),
|
|
296
|
jobListingsContainer.leadingAnchor.constraint(equalTo: jobListingsScrollView.contentView.leadingAnchor),
|
298
|
jobListingsContainer.leadingAnchor.constraint(equalTo: jobListingsScrollView.contentView.leadingAnchor),
|
|
297
|
jobListingsContainer.widthAnchor.constraint(equalTo: jobListingsScrollView.contentView.widthAnchor),
|
299
|
jobListingsContainer.widthAnchor.constraint(equalTo: jobListingsScrollView.contentView.widthAnchor),
|
|
|
|
300
|
+ jobListingsScrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200),
|
|
298
|
|
301
|
|
|
299
|
greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
|
302
|
greetingLabel.leadingAnchor.constraint(equalTo: mainOverlay.leadingAnchor, constant: 16),
|
|
300
|
greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -16),
|
303
|
greetingLabel.trailingAnchor.constraint(equalTo: mainOverlay.trailingAnchor, constant: -16),
|
|
@@ -1209,7 +1212,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
1209
|
case .success(let output):
|
1212
|
case .success(let output):
|
|
1210
|
let normalizedJobs = self.normalizedJobs(output.jobs)
|
1213
|
let normalizedJobs = self.normalizedJobs(output.jobs)
|
|
1211
|
self.configureJobListings(normalizedJobs, noResultsForQuery: prompt)
|
1214
|
self.configureJobListings(normalizedJobs, noResultsForQuery: prompt)
|
|
1212
|
- let reply = self.makeAssistantSearchReply(query: prompt, jobs: normalizedJobs, jsonResult: output.rawJSON)
|
|
|
|
|
|
1215
|
+ let reply = self.makeAssistantSearchReply(query: prompt, jobs: normalizedJobs)
|
|
1213
|
self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
|
1216
|
self.chatMessages.append(ChatMessage(role: "assistant", content: reply))
|
|
1214
|
self.appendChatBubble(text: reply, isUser: false)
|
1217
|
self.appendChatBubble(text: reply, isUser: false)
|
|
1215
|
self.chatStatusLabel.stringValue = "Ask for another job, company, or skill match"
|
1218
|
self.chatStatusLabel.stringValue = "Ask for another job, company, or skill match"
|
|
@@ -1234,42 +1237,16 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
1234
|
return trimmed.filter { !$0.title.isEmpty && !$0.description.isEmpty }
|
1237
|
return trimmed.filter { !$0.title.isEmpty && !$0.description.isEmpty }
|
|
1235
|
}
|
1238
|
}
|
|
1236
|
|
1239
|
|
|
1237
|
- private func makeAssistantSearchReply(query: String, jobs: [JobListing], jsonResult: String) -> String {
|
|
|
|
|
|
1240
|
+ private func makeAssistantSearchReply(query: String, jobs: [JobListing]) -> String {
|
|
1238
|
if jobs.isEmpty {
|
1241
|
if jobs.isEmpty {
|
|
1239
|
- return """
|
|
|
|
1240
|
- No jobs were found for "\(query)".
|
|
|
|
1241
|
-
|
|
|
|
1242
|
- JSON result:
|
|
|
|
1243
|
- \(jsonResult)
|
|
|
|
1244
|
- """
|
|
|
|
1245
|
- }
|
|
|
|
1246
|
- let rows = jobs.prefix(8).enumerated().map { index, job in
|
|
|
|
1247
|
- let fallback = "https://www.indeed.com/jobs?q=\(job.title.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")"
|
|
|
|
1248
|
- let link = (job.url?.isEmpty == false) ? job.url! : fallback
|
|
|
|
1249
|
- let compactDescription = compactSingleLine(job.description, maxCharacters: 110)
|
|
|
|
1250
|
- return "\(index + 1)) \(job.title) | \(compactDescription) | \(link)"
|
|
|
|
|
|
1242
|
+ return "No jobs were found for \"\(query)\". Try another title, skill, company, or location."
|
|
1251
|
}
|
1243
|
}
|
|
1252
|
return """
|
1244
|
return """
|
|
1253
|
Found \(jobs.count) job result(s) for "\(query)".
|
1245
|
Found \(jobs.count) job result(s) for "\(query)".
|
|
1254
|
-
|
|
|
|
1255
|
- Row-wise results:
|
|
|
|
1256
|
- \(rows.joined(separator: "\n"))
|
|
|
|
1257
|
-
|
|
|
|
1258
|
- JSON result:
|
|
|
|
1259
|
- \(jsonResult)
|
|
|
|
|
|
1246
|
+ Each result is shown as a card with the role, job description, and an Apply button that opens the job link.
|
|
1260
|
"""
|
1247
|
"""
|
|
1261
|
}
|
1248
|
}
|
|
1262
|
|
1249
|
|
|
1263
|
- private func compactSingleLine(_ text: String, maxCharacters: Int) -> String {
|
|
|
|
1264
|
- let single = text
|
|
|
|
1265
|
- .replacingOccurrences(of: "\n", with: " ")
|
|
|
|
1266
|
- .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
|
|
|
|
1267
|
- .trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
1268
|
- guard single.count > maxCharacters, maxCharacters > 1 else { return single }
|
|
|
|
1269
|
- let end = single.index(single.startIndex, offsetBy: maxCharacters - 1)
|
|
|
|
1270
|
- return String(single[..<end]) + "…"
|
|
|
|
1271
|
- }
|
|
|
|
1272
|
-
|
|
|
|
1273
|
func controlTextDidBeginEditing(_ obj: Notification) {
|
1250
|
func controlTextDidBeginEditing(_ obj: Notification) {
|
|
1274
|
applySearchFieldInsertionPoint(obj.object)
|
1251
|
applySearchFieldInsertionPoint(obj.object)
|
|
1275
|
if (obj.object as? NSTextField) === jobKeywordsField {
|
1252
|
if (obj.object as? NSTextField) === jobKeywordsField {
|
|
@@ -1665,7 +1642,7 @@ private final class OpenAIJobSearchService {
|
|
1665
|
let cleanedText = Self.extractJSONObject(from: text)
|
1642
|
let cleanedText = Self.extractJSONObject(from: text)
|
|
1666
|
let jsonData = Data(cleanedText.utf8)
|
1643
|
let jsonData = Data(cleanedText.utf8)
|
|
1667
|
let jobsPayload = try JSONDecoder().decode(JobSearchResultsPayload.self, from: jsonData)
|
1644
|
let jobsPayload = try JSONDecoder().decode(JobSearchResultsPayload.self, from: jsonData)
|
|
1668
|
- completion(.success(JobSearchOutput(jobs: jobsPayload.jobs, rawJSON: cleanedText)))
|
|
|
|
|
|
1645
|
+ completion(.success(JobSearchOutput(jobs: jobsPayload.jobs)))
|
|
1669
|
} catch {
|
1646
|
} catch {
|
|
1670
|
let rawBody = String(data: data, encoding: .utf8) ?? "<non-utf8 response>"
|
1647
|
let rawBody = String(data: data, encoding: .utf8) ?? "<non-utf8 response>"
|
|
1671
|
let message = "The API response could not be parsed as job JSON. Raw response: \(rawBody.prefix(600))"
|
1648
|
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 {
|
|
1772
|
|
1749
|
|
|
1773
|
private struct JobSearchOutput {
|
1750
|
private struct JobSearchOutput {
|
|
1774
|
let jobs: [JobListing]
|
1751
|
let jobs: [JobListing]
|
|
1775
|
- let rawJSON: String
|
|
|
|
1776
|
}
|
1752
|
}
|
|
1777
|
|
1753
|
|
|
1778
|
private struct OpenAIAPIErrorResponse: Codable {
|
1754
|
private struct OpenAIAPIErrorResponse: Codable {
|