diff --git a/ios/Keep/Models/Note.swift b/ios/Keep/Models/Note.swift index b1c4e4e..61a359f 100644 --- a/ios/Keep/Models/Note.swift +++ b/ios/Keep/Models/Note.swift @@ -37,3 +37,8 @@ struct Note: Identifiable, Codable, Equatable { struct NotesResponse: Codable { let notes: [Note] } struct NoteResponse: Codable { let note: Note } struct OkResponse: Codable { let ok: Bool } +/// POST /api/notes/title — model-generated (or heuristic) note metadata. +struct NoteMeta: Codable { + let title: String + let summary: String? +} diff --git a/ios/Keep/Services/KeepAPI.swift b/ios/Keep/Services/KeepAPI.swift index 3e66891..af96f5b 100644 --- a/ios/Keep/Services/KeepAPI.swift +++ b/ios/Keep/Services/KeepAPI.swift @@ -36,14 +36,25 @@ actor KeepAPI { try await request("/api/notes", method: "GET", as: NotesResponse.self).notes } - func create(body: String) async throws -> Note { - try await request( + func create(body: String, title: String? = nil, summary: String? = nil) async throws -> Note { + var json: [String: Any] = ["body": body] + if let title { json["title"] = title } + if let summary, !summary.isEmpty { json["summary"] = summary } + return try await request( "/api/notes", method: "POST", - json: ["body": body], + json: json, as: NoteResponse.self ).note } + func generateMeta(body: String) async throws -> NoteMeta { + try await request( + "/api/notes/title", method: "POST", + json: ["body": body], + as: NoteMeta.self + ) + } + func update(id: String, patch: [String: Any]) async throws -> Note { try await request( "/api/notes/\(id)", method: "PATCH", diff --git a/ios/Keep/Services/NotesStore.swift b/ios/Keep/Services/NotesStore.swift index 3d7b76a..6cbc82f 100644 --- a/ios/Keep/Services/NotesStore.swift +++ b/ios/Keep/Services/NotesStore.swift @@ -41,7 +41,8 @@ final class NotesStore { @discardableResult func create(body: String) async -> Note? { do { - let note = try await api.create(body: body) + let meta = try? await api.generateMeta(body: body) + let note = try await api.create(body: body, title: meta?.title, summary: meta?.summary) notes.insert(note, at: 0) return note } catch { @@ -50,13 +51,31 @@ final class NotesStore { } } + /// Body saves regenerate the title/summary when the lead line changes (or + /// the note has no title yet), folded into the same PATCH — mirroring the + /// web's autosave. Meta failures degrade to a plain body save. func update(_ id: String, patch: [String: Any]) async { + var patch = patch + if let body = patch["body"] as? String, + patch["title"] == nil, + let current = notes.first(where: { $0.id == id }), + Self.firstLine(body) != Self.firstLine(current.body) + || current.title.trimmingCharacters(in: .whitespaces).isEmpty, + let meta = try? await api.generateMeta(body: body) { + patch["title"] = meta.title + if let summary = meta.summary, !summary.isEmpty { patch["summary"] = summary } + } do { let updated = try await api.update(id: id, patch: patch) if let i = notes.firstIndex(where: { $0.id == id }) { notes[i] = updated } } catch { handle(error) } } + private static func firstLine(_ body: String) -> String { + (body.components(separatedBy: "\n").first ?? "") + .trimmingCharacters(in: .whitespaces) + } + func togglePin(_ note: Note) async { await update(note.id, patch: ["pinned": !note.pinned]) }