Files
openclaw/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel+Attachments.swift
2026-07-12 18:33:47 +02:00

128 lines
4.1 KiB
Swift

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(
format: 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 = String(localized: "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 = String(
format: String(localized: "Could not process %1$@: %2$@"),
fileName,
error.localizedDescription)
return
}
if processed.count > Self.maxAttachmentBytes {
self.errorText = String(
format: String(localized: "Attachment %@ exceeds 5 MB limit after resizing"),
fileName)
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
}
}