Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

101 lines
4.3 KiB
Swift

// Source of truth for the per-model settings "Preset" chip strip. Mirrors
// HTML's localStorage-cached preset bundle:
//
// 1. Disk cache (~/Library/Application Support/oMLX/preset_cache.json)
// 2. Bundled fixture (Contents/Resources/omlx/admin/static/omlx_preset.json)
// — staged into the .app by Scripts/build.sh from the omlx package.
// 3. Remote refresh via POST /admin/api/presets/refresh (omlx.ai proxy).
//
// Replaces the previous behavior of treating `/api/profile-templates`
// entries with `is_builtin: true` as presets. Server-side builtins are
// retired (see Phase 1 — omlx/template/default_global_profile.json
// deleted), so the JSON bundle is now the only preset source for both
// Swift and HTML.
import Foundation
import SwiftUI
@MainActor
@Observable
final class PresetBundleStore {
private(set) var entries: [PresetEntry] = []
private(set) var isRefreshing: Bool = false
private(set) var lastError: String?
/// Where we persist the most recent successful bundle. Same role as
/// HTML's `localStorage.omlx_preset_cache`.
private let cacheURL: URL
init() {
self.cacheURL = Self.defaultCacheURL()
// Load synchronously so the first SwiftUI render of the preset
// chip strip already has entries — avoids a flash of empty state
// while a background load completes.
if let bundle = Self.loadDisk(at: cacheURL) ?? Self.loadFixture() {
self.entries = bundle.presets
}
}
/// POST /admin/api/presets/refresh, persist the response, swap in the
/// new entries. On failure, `lastError` is set and `entries` is left
/// untouched so the chip strip keeps its current contents.
func refresh(client: OMLXClient) async {
guard !isRefreshing else { return }
isRefreshing = true
defer { isRefreshing = false }
do {
let bundle = try await client.refreshPresetBundle()
self.entries = bundle.presets
self.lastError = nil
Self.writeDisk(bundle, to: cacheURL)
} catch {
self.lastError = (error as? OMLXClientError)
.map { String(describing: $0) }
?? error.localizedDescription
}
}
// MARK: - Helpers
private static func defaultCacheURL() -> URL {
let base = FileManager.default.urls(
for: .applicationSupportDirectory, in: .userDomainMask
).first ?? FileManager.default.temporaryDirectory
let dir = base.appendingPathComponent("oMLX", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent("preset_cache.json")
}
private static func loadDisk(at url: URL) -> PresetBundleDTO? {
guard FileManager.default.fileExists(atPath: url.path),
let data = try? Data(contentsOf: url) else { return nil }
return try? JSONDecoder().decode(PresetBundleDTO.self, from: data)
}
/// Pull the JSON bundled inside the app at
/// `Contents/Resources/omlx/admin/static/omlx_preset.json`. The file
/// is staged into the app by `Scripts/build.sh` (omlx package rsync),
/// not by Xcode — so this lookup uses the on-disk path under
/// `Bundle.main.resourceURL` rather than `Bundle.main.url(forResource:)`
/// which only finds Xcode-managed resources.
private static func loadFixture() -> PresetBundleDTO? {
guard let resources = Bundle.main.resourceURL else { return nil }
let url = resources
.appendingPathComponent("omlx", isDirectory: true)
.appendingPathComponent("admin", isDirectory: true)
.appendingPathComponent("static", isDirectory: true)
.appendingPathComponent("omlx_preset.json")
guard FileManager.default.fileExists(atPath: url.path),
let data = try? Data(contentsOf: url) else { return nil }
return try? JSONDecoder().decode(PresetBundleDTO.self, from: data)
}
private static func writeDisk(_ bundle: PresetBundleDTO, to url: URL) {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
guard let data = try? encoder.encode(bundle) else { return }
try? data.write(to: url, options: [.atomic])
}
}