import Foundation enum LLMError: LocalizedError { case notConfigured case networkError(String) case apiError(String) case noContent var errorDescription: String? { switch self { case .notConfigured: return "OpenAI API Key fehlt. Bitte in den Einstellungen hinterlegen." case .networkError(let msg): return "Verbindungsproblem: \(msg)" case .apiError(let msg): return "Fehler von OpenAI: \(msg)" case .noContent: return "Keine Antwort erhalten. Bitte nochmal versuchen." } } } enum RewriteModel: String { case fastEdit = "gpt-4o-mini" case rageMode = "gpt-4o" } private struct OpenAIChatRequest: Encodable { struct Message: Encodable { let role: String let content: String } let model: String let messages: [Message] let temperature: Double } private struct OpenAIChatResponse: Decodable { struct Choice: Decodable { struct Message: Decodable { let content: String? } let message: Message? } let choices: [Choice]? } private struct OpenAIErrorResponse: Decodable { struct APIError: Decodable { let message: String? } let error: APIError? } enum LLMService { private static let chatCompletionsURL = URL(string: "https://api.openai.com/v1/chat/completions")! private static let session: URLSession = { let configuration = URLSessionConfiguration.ephemeral configuration.waitsForConnectivity = false configuration.requestCachePolicy = .reloadIgnoringLocalCacheData configuration.timeoutIntervalForRequest = 45 configuration.timeoutIntervalForResource = 45 return URLSession(configuration: configuration) }() static func improve( text: String, settings: TextImprovementSettings, model: RewriteModel = .fastEdit ) async throws -> String { try await complete( text: text, systemPrompt: buildSystemPrompt(settings: settings), model: model, temperature: 0.3 ) } static func dampfAblassen( text: String, systemPrompt: String, model: RewriteModel = .rageMode ) async throws -> String { try await complete( text: text, systemPrompt: systemPrompt, model: model, temperature: 0.4 ) } static func addEmojis( text: String, settings: EmojiTextSettings, model: RewriteModel = .fastEdit ) async throws -> String { try await complete( text: text, systemPrompt: buildEmojiSystemPrompt(density: settings.emojiDensity), model: model, temperature: 0.3 ) } private static func complete( text: String, systemPrompt: String, model: RewriteModel, temperature: Double ) async throws -> String { guard let apiKey = KeychainService.load(key: .openAIAPIKey) else { throw LLMError.notConfigured } let payload = OpenAIChatRequest( model: model.rawValue, messages: [ .init(role: "system", content: systemPrompt), .init(role: "user", content: text), ], temperature: temperature ) var request = URLRequest(url: chatCompletionsURL) request.httpMethod = "POST" request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.timeoutInterval = 45 request.httpBody = try JSONEncoder().encode(payload) let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw LLMError.networkError("Keine gültige Antwort") } guard httpResponse.statusCode == 200 else { throw LLMError.apiError(openAIErrorMessage(from: data) ?? "Status \(httpResponse.statusCode)") } let result = try JSONDecoder().decode(OpenAIChatResponse.self, from: data) guard let content = result.choices?.first?.message?.content, !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw LLMError.noContent } return content.trimmingCharacters(in: .whitespacesAndNewlines) } private static func openAIErrorMessage(from data: Data) -> String? { (try? JSONDecoder().decode(OpenAIErrorResponse.self, from: data))?.error?.message } private static func buildEmojiSystemPrompt(density: EmojiTextSettings.EmojiDensity) -> String { let densityInstruction: String switch density { case .wenig: densityInstruction = "Setze nur vereinzelt Emojis ein, maximal 1-2 pro Absatz." case .mittel: densityInstruction = "Setze regelmaessig passende Emojis ein, etwa alle 1-2 Saetze." case .viel: densityInstruction = "Setze grosszuegig Emojis ein, gerne mehrere pro Satz." } return "Du erhaeltst ein gesprochenes Transkript. Gib den Text moeglichst originalgetreu zurueck, aber fuege passende Emojis ein. \(densityInstruction) Korrigiere offensichtliche Sprach- und Grammatikfehler. Behalte den Stil und die Bedeutung bei. Gib NUR den Text mit Emojis zurueck, keine Erklaerungen." } private static func buildSystemPrompt(settings: TextImprovementSettings) -> String { if !settings.systemPrompt.isEmpty { var prompt = settings.systemPrompt if !settings.customTerms.isEmpty { prompt += "\n\nWichtig: Diese Eigennamen und Fachbegriffe muessen exakt so geschrieben werden: \(settings.customTerms.joined(separator: ", "))" } return prompt } var prompt = """ Du bist ein Lektor und Schreibassistent. Verbessere den folgenden Text: - Korrigiere Rechtschreibung und Grammatik - Verbessere die Formulierung und den Lesefluss - Behalte die urspruengliche Bedeutung bei - Gib NUR den verbesserten Text zurueck, keine Erklaerungen """ switch settings.tone { case .formal: prompt += "\n- Verwende einen formellen, professionellen Ton" case .neutral: prompt += "\n- Verwende einen neutralen, klaren Ton" case .casual: prompt += "\n- Verwende einen lockeren, natuerlichen Ton" } if !settings.customTerms.isEmpty { prompt += "\n\nWichtig: Diese Eigennamen und Fachbegriffe muessen exakt so geschrieben werden: \(settings.customTerms.joined(separator: ", "))" } if !settings.context.isEmpty { prompt += "\n\nKontext: \(settings.context)" } return prompt } }