Some checks failed
CI / build-and-deploy (push) Failing after 6s
Device auto-registers on first launch using hardcoded backend URL and anon key (MarksAuth.swift). 90-day JWT stored in UserDefaults, refreshed transparently. No settings fields needed — ClaudeService is always active and non-optional throughout the app. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
4.8 KiB
Swift
114 lines
4.8 KiB
Swift
import Foundation
|
|
|
|
struct PodcastStatus: Decodable {
|
|
let status: String
|
|
let progress: Int
|
|
let title: String?
|
|
let error: String?
|
|
|
|
var isDone: Bool { status == "done" }
|
|
var isFailed: Bool { status == "error" }
|
|
}
|
|
|
|
struct ClaudeService: Sendable {
|
|
|
|
// MARK: - Bookmark AI
|
|
|
|
func enrich(bookmark: Bookmark) async throws -> (summary: String, tags: [String]) {
|
|
struct Response: Decodable { let summary: String; let tags: [String] }
|
|
let body: [String: Any] = [
|
|
"url": bookmark.url,
|
|
"title": bookmark.displayTitle,
|
|
"tags": bookmark.tagNames,
|
|
]
|
|
let data = try await post("/v1/marks/enrich", body: body)
|
|
let decoded = try JSONDecoder().decode(Response.self, from: data)
|
|
return (decoded.summary, decoded.tags)
|
|
}
|
|
|
|
func semanticSearch(query: String, in bookmarks: [Bookmark]) async throws -> [Bookmark] {
|
|
guard !bookmarks.isEmpty else { return [] }
|
|
struct Response: Decodable { let indices: [Int] }
|
|
let list: [[String: Any]] = bookmarks.map { b in
|
|
var d: [String: Any] = ["id": b.id, "title": b.displayTitle, "domain": b.domain]
|
|
if let s = b.aiSummary { d["summary"] = s }
|
|
return d
|
|
}
|
|
let data = try await post("/v1/marks/search", body: ["query": query, "bookmarks": list])
|
|
let decoded = try JSONDecoder().decode(Response.self, from: data)
|
|
return decoded.indices.compactMap { i in i < bookmarks.count ? bookmarks[i] : nil }
|
|
}
|
|
|
|
func generateCollections(from bookmarks: [Bookmark]) async throws -> [SmartCollection] {
|
|
guard !bookmarks.isEmpty else { return [] }
|
|
struct Item: Decodable { let name: String; let description: String; let bookmarkIds: [Int] }
|
|
struct Response: Decodable { let collections: [Item] }
|
|
let capped = Array(bookmarks.prefix(150))
|
|
let list: [[String: Any]] = capped.map { b in
|
|
["id": b.id, "title": b.displayTitle, "tags": b.tagNames]
|
|
}
|
|
let data = try await post("/v1/marks/collections", body: ["bookmarks": list])
|
|
let decoded = try JSONDecoder().decode(Response.self, from: data)
|
|
return decoded.collections.map {
|
|
SmartCollection(name: $0.name, description: $0.description, bookmarkIds: $0.bookmarkIds)
|
|
}
|
|
}
|
|
|
|
// MARK: - Podcast
|
|
|
|
func generatePodcast(url: String) async throws -> String {
|
|
struct Response: Decodable { let job_id: String }
|
|
let data = try await post("/v1/podcast/generate", body: ["url": url])
|
|
return try JSONDecoder().decode(Response.self, from: data).job_id
|
|
}
|
|
|
|
func podcastStatus(jobId: String) async throws -> PodcastStatus {
|
|
let data = try await get("/v1/podcast/status/\(jobId)")
|
|
return try JSONDecoder().decode(PodcastStatus.self, from: data)
|
|
}
|
|
|
|
func downloadPodcastAudio(jobId: String) async throws -> URL {
|
|
let data = try await get("/v1/podcast/audio/\(jobId)")
|
|
let dest = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("podcast-\(jobId).mp3")
|
|
try data.write(to: dest, options: .atomic)
|
|
return dest
|
|
}
|
|
|
|
// MARK: - HTTP primitives
|
|
|
|
private func post(_ path: String, body: [String: Any]) async throws -> Data {
|
|
let token = try await MarksAuth.validToken()
|
|
guard let url = URL(string: MarksAuth.baseURL + path) else { throw APIError.invalidUrl }
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
print("[AI] POST \(path)")
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
|
|
print("[AI] POST \(path) → \(status)")
|
|
guard (200...299).contains(status) else {
|
|
print("[AI] error: \(String(data: data, encoding: .utf8)?.prefix(200) ?? "")")
|
|
throw APIError.badStatus(status)
|
|
}
|
|
return data
|
|
}
|
|
|
|
private func get(_ path: String) async throws -> Data {
|
|
let token = try await MarksAuth.validToken()
|
|
guard let url = URL(string: MarksAuth.baseURL + path) else { throw APIError.invalidUrl }
|
|
var request = URLRequest(url: url)
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
print("[AI] GET \(path)")
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
|
|
print("[AI] GET \(path) → \(status)")
|
|
guard (200...299).contains(status) else {
|
|
throw APIError.badStatus(status)
|
|
}
|
|
return data
|
|
}
|
|
}
|