import Foundation import OpenClawKit import UniformTypeIdentifiers #if canImport(AppKit) import AppKit #elseif canImport(UIKit) import UIKit #endif extension OpenClawChatViewModel { /// Stages a recorded m4a voice note and removes its temporary file. public func addVoiceNoteAttachment(fileURL: URL, durationSeconds: Double) async { self.beginAttachmentStaging() defer { try? FileManager.default.removeItem(at: fileURL) self.endAttachmentStaging() } let data: Data do { data = try await Task.detached(priority: .userInitiated) { try Data(contentsOf: fileURL) }.value } catch { self.errorText = String(localized: "Could not attach voice note: \(error.localizedDescription)") return } guard data.count <= Self.maxAttachmentBytes else { self.errorText = String(localized: "Voice note exceeds the 5 MB attachment limit") return } let normalizedDuration = durationSeconds.isFinite ? min(max(0, durationSeconds), OpenClawVoiceNoteRecorder.maximumDurationSeconds) : 0 self.attachments.append( OpenClawPendingAttachment( url: nil, data: data, fileName: fileURL.lastPathComponent, mimeType: "audio/mp4", preview: nil, durationSeconds: normalizedDuration)) } func loadAttachments(urls: [URL]) async { for url in urls { do { let data = try await Task.detached { try Data(contentsOf: url) }.value await self.addImageAttachment( url: url, data: data, fileName: url.lastPathComponent, mimeType: Self.mimeType(for: url) ?? "application/octet-stream") } catch { await MainActor.run { self.errorText = error.localizedDescription } } } } static func mimeType(for url: URL) -> String? { let ext = url.pathExtension guard !ext.isEmpty else { return nil } return (UTType(filenameExtension: ext) ?? .data).preferredMIMEType } func addImageAttachment(url: URL?, data: Data, fileName: String, mimeType: String) async { let uti: UTType = { if let url { return UTType(filenameExtension: url.pathExtension) ?? .data } return UTType(mimeType: mimeType) ?? .data }() guard uti.conforms(to: .image) else { self.errorText = "Only image attachments are supported right now" return } let processed: Data do { processed = try await Task.detached(priority: .userInitiated) { try ChatImageProcessor.processForUpload(data: data) }.value } catch { self.errorText = "Could not process \(fileName): \(error.localizedDescription)" return } if processed.count > Self.maxAttachmentBytes { self.errorText = "Attachment \(fileName) exceeds 5 MB limit after resizing" return } let outputFileName: String = { let baseName = (fileName as NSString).deletingPathExtension return baseName.isEmpty ? "image.jpg" : "\(baseName).jpg" }() let preview = Self.previewImage(data: processed) self.attachments.append( OpenClawPendingAttachment( url: url, data: processed, fileName: outputFileName, mimeType: "image/jpeg", preview: preview)) } static func previewImage(data: Data) -> OpenClawPlatformImage? { #if canImport(AppKit) NSImage(data: data) #elseif canImport(UIKit) UIImage(data: data) #else nil #endif } }