| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- import Foundation
- protocol EmailWriterServicing: Sendable {
- func generateEmail(
- prompt: String,
- subject: String,
- emailType: EmailType,
- tone: EmailTone
- ) async throws -> String
- }
- struct EmailWriterService: EmailWriterServicing {
- private let openAI: OpenAIChatServicing
- init(openAI: OpenAIChatServicing = OpenAIChatService.shared) {
- self.openAI = openAI
- }
- func generateEmail(
- prompt: String,
- subject: String,
- emailType: EmailType,
- tone: EmailTone
- ) async throws -> String {
- let trimmed = prompt.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmed.isEmpty else {
- throw OpenAIServiceError.apiError("Describe what the email should say.")
- }
- var userPrompt = "Email type: \(emailType.title)\nTone: \(tone.title)\n"
- let trimmedSubject = subject.trimmingCharacters(in: .whitespacesAndNewlines)
- if !trimmedSubject.isEmpty {
- userPrompt += "Subject: \(trimmedSubject)\n"
- }
- userPrompt += "\nContent instructions:\n\(trimmed)"
- return try await openAI.completeText(
- systemPrompt: """
- You are a professional email writer. Write a complete, ready-to-send email based on the \
- user's instructions. Include a subject line only if one was not already provided. \
- Return only the email text with no preamble or explanation.
- """,
- userPrompt: userPrompt
- )
- }
- }
- enum EmailWriterDefaults {
- static let service: any EmailWriterServicing = EmailWriterService()
- }
- enum EmailWriterWordLimit {
- static func message(maxWords: Int) -> String {
- "Maximum \(maxWords) words allowed."
- }
- static func count(in text: String) -> Int {
- let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmed.isEmpty else { return 0 }
- return trimmed
- .components(separatedBy: .whitespacesAndNewlines)
- .filter { !$0.isEmpty }
- .count
- }
- static func apply(_ text: String, maxWords: Int) -> (text: String, exceeded: Bool) {
- guard count(in: text) > maxWords else { return (text, false) }
- return (truncated(text, toMaxWords: maxWords), true)
- }
- static func applyImported(
- _ text: String,
- maxWords: Int,
- setError: (String?) -> Void
- ) -> String {
- let (limited, exceeded) = apply(text, maxWords: maxWords)
- setError(exceeded ? message(maxWords: maxWords) : nil)
- return limited
- }
- private static func truncated(_ text: String, toMaxWords maxWords: Int) -> String {
- guard maxWords > 0 else { return "" }
- guard count(in: text) > maxWords else { return text }
- var wordsSeen = 0
- var index = text.startIndex
- while index < text.endIndex {
- while index < text.endIndex, text[index].isWhitespace {
- index = text.index(after: index)
- }
- guard index < text.endIndex else { break }
- wordsSeen += 1
- while index < text.endIndex, !text[index].isWhitespace {
- index = text.index(after: index)
- }
- if wordsSeen >= maxWords {
- return String(text[..<index])
- }
- }
- return text
- }
- }
- enum EmailWriterErrors {
- static func message(for error: Error) -> String {
- UserFacingError.message(for: error)
- }
- }
- enum EmailType: String, CaseIterable, Identifiable, Codable {
- case followUp
- case thankYou
- case introduction
- case apology
- case request
- case meetingInvite
- case salesOutreach
- case custom
- var id: String { rawValue }
- var title: String {
- switch self {
- case .followUp: "Follow-up"
- case .thankYou: "Thank You"
- case .introduction: "Introduction"
- case .apology: "Apology"
- case .request: "Request"
- case .meetingInvite: "Meeting Invite"
- case .salesOutreach: "Sales Outreach"
- case .custom: "Custom"
- }
- }
- }
- enum EmailTone: String, CaseIterable, Identifiable, Codable {
- case professional
- case friendly
- case formal
- case casual
- case persuasive
- var id: String { rawValue }
- var title: String {
- switch self {
- case .professional: "Professional"
- case .friendly: "Friendly"
- case .formal: "Formal"
- case .casual: "Casual"
- case .persuasive: "Persuasive"
- }
- }
- }
|