// Visual components used by the Profiles tab and the Server-defaults block // on the Server screen. The shapes mirror omlx-screens.jsx:626-1191: // // • ProfileGroup — chip row labelled by scope // • ActiveProfileBanner — top-of-tab summary of the model's attach state // • SaveAsPopover — inline name + scope toggle for "Save as new" // • ProfileDetailCard — visual summary of one profile's settings // // The view models live elsewhere — this file is render-only. import SwiftUI // MARK: - Scope colors / labels /// Per-scope visual treatment. Lifted from omlx-screens.jsx:878-884 /// (SCOPE_META) so the chip dots and badges read the same as the canvas. enum ProfileScopeMeta { static func color(_ scope: ProfileScope, theme: OMLXTheme) -> Color { switch scope { case .preset: return theme.amberDot case .global: return Color(rgb24: 0xAF52DE) case .model: return theme.blueDot } } static func label(_ scope: ProfileScope) -> String { switch scope { case .preset: return String(localized: "profile.scope.preset", defaultValue: "Preset", comment: "Scope label for shipped preset profiles in Profiles tab") case .global: return String(localized: "profile.scope.global", defaultValue: "Global", comment: "Scope label for user-defined global profiles") case .model: return String(localized: "profile.scope.model", defaultValue: "Model", comment: "Scope label for per-model profiles") } } } // MARK: - ProfileGroup /// One chip row, scoped to preset / global / model. The active chip /// (when its scope matches `activeName`) renders in scope color; the /// "based-on of the working profile" gets a dashed scope-color border; /// preview-selected gets a 1px primary-text border. struct ProfileGroup: View { let scope: ProfileScope let label: String let names: [String] /// Name of the currently-active profile in this scope (or nil if the /// active profile lives in a different scope). let activeName: String? /// Name of the profile the working profile forked from (or nil). let basedOnName: String? /// Name of the profile currently being previewed in the detail card. let previewName: String? let canSaveCurrent: Bool let onSelect: (String) -> Void let onSaveCurrent: () -> Void /// Optional refresh affordance — when non-nil, a small refresh icon /// renders in the header instead of (or alongside) the save button. /// Used by the preset chip strip to pull the latest bundle from /// omlx.ai via `POST /api/presets/refresh`. var onRefresh: (() -> Void)? = nil /// Spinner state for the refresh icon. Disables the button while a /// refresh is in flight so a fast user can't queue duplicates. var isRefreshing: Bool = false /// Optional inline-rename callback. When provided, double-clicking a /// chip swaps its label for a `TextField`; Enter commits via /// `(originalName, newName)`. Pass nil for read-only groups (preset /// chips backed by the shipped JSON bundle). var onRename: ((String, String) -> Void)? = nil @Environment(\.omlxTheme) private var theme @State private var renamingName: String? = nil @State private var renameText: String = "" @FocusState private var renameFieldFocused: Bool var body: some View { VStack(alignment: .leading, spacing: 6) { header chipRow } .padding(.horizontal, 14) .padding(.bottom, 12) } @ViewBuilder private var header: some View { HStack(spacing: 8) { Circle() .fill(ProfileScopeMeta.color(scope, theme: theme)) .frame(width: 6, height: 6) Text(label) .font(.omlxText(10.5, weight: .heavy)) .kerning(0.7) .textCase(.uppercase) .foregroundStyle(theme.textSecondary) Spacer(minLength: 8) if let onRefresh { Button { onRefresh() } label: { Image(systemName: isRefreshing ? "arrow.triangle.2.circlepath" : "arrow.clockwise") .font(.system(size: 11, weight: .semibold)) .symbolEffect(.rotate, isActive: isRefreshing) } .buttonStyle(.omlx(.plain, size: .small)) .help(String(localized: "profile.group.refresh.help", defaultValue: "Refresh presets from omlx.ai", comment: "Tooltip on the refresh button in the preset chip group header")) .disabled(isRefreshing) } if canSaveCurrent { Button { onSaveCurrent() } label: { Label(String(localized: "profile.group.save_current_as_new", defaultValue: "Save current as new", comment: "Button in profile chip-group header that saves the working profile as a new named profile"), systemImage: "plus") .labelStyle(.titleAndIcon) .font(.omlxText(11, weight: .medium)) } .buttonStyle(.omlx(.plain, size: .small)) .overlay( Capsule().strokeBorder( theme.inputBorder, style: StrokeStyle(lineWidth: 1, dash: [3, 2]) ) ) .foregroundStyle(theme.textSecondary) } } .padding(.horizontal, 2) } @ViewBuilder private var chipRow: some View { let metaColor = ProfileScopeMeta.color(scope, theme: theme) Group { if names.isEmpty { Text(String(localized: "profile.group.empty", defaultValue: "No \(ProfileScopeMeta.label(scope).lowercased()) profiles yet.", comment: "Placeholder text in an empty profile chip group; placeholder is the lowercased scope name")) .font(.omlxText(11.5)) .foregroundStyle(theme.textTertiary) .padding(.vertical, 10) .padding(.horizontal, 12) } else { FlowLayout(spacing: 6) { ForEach(names, id: \.self) { name in chip(name: name, metaColor: metaColor) } } .padding(.vertical, 8) .padding(.horizontal, 10) } } // The chip row needs to span its container's width so FlowLayout // receives a definite width proposal — without this the layout's // two passes (sizeThatFits with .unspecified, then placeSubviews // with the parent's bounds) disagree on wrap count and chips // overflow into the next group's header. `.background` (vs the // old ZStack) keeps the rounded surface flush to the same frame. .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(theme.groupBg) ) .overlay( RoundedRectangle(cornerRadius: 10, style: .continuous) .strokeBorder(theme.groupBorder, lineWidth: 0.5) ) } @ViewBuilder private func chip(name: String, metaColor: Color) -> some View { let isActive = (activeName == name) let isBase = (basedOnName == name) let isPreviewed = (previewName == name) let isEditing = (renamingName == name) HStack(spacing: 5) { if isActive && !isEditing { Image(systemName: "checkmark") .font(.system(size: 10, weight: .bold)) .foregroundStyle(.white) } if isEditing { TextField("", text: $renameText) .textFieldStyle(.plain) .font(.omlxText(12, weight: .medium)) .foregroundStyle(isActive ? .white : theme.text) .fixedSize(horizontal: true, vertical: false) .focused($renameFieldFocused) .onSubmit { commitRename(original: name) } .onExitCommand { renamingName = nil } .onChange(of: renameFieldFocused) { _, focused in // Commit on focus loss (click outside / tab away) // — same validation path as Enter. The TextField // only loses focus *after* `onSubmit` runs, and // `commitRename` clears `renamingName` before // returning, so this guard prevents a double-fire // on a clean Enter. Escape still cancels via // `.onExitCommand` (which nils `renamingName` // before focus loss propagates here). if !focused && renamingName == name { commitRename(original: name) } } } else { Text(name) .font(.omlxText(12, weight: .medium)) .foregroundStyle(isActive ? .white : theme.text) } } .padding(.horizontal, 11) .frame(height: 26) .background( Capsule().fill( isActive ? metaColor : (isPreviewed ? theme.selBg : theme.codeBg) ) ) .overlay( Capsule().strokeBorder( isActive ? Color.clear : (isPreviewed ? theme.text : (isBase ? metaColor.opacity(0.6) : theme.inputBorder)), style: StrokeStyle( lineWidth: isPreviewed ? 1 : 0.5, dash: (isBase && !isActive && !isPreviewed) ? [3, 2] : [] ) ) ) .contentShape(Capsule()) .gesture( // ExclusiveGesture(first, second): the double-tap is tried // first; if it succeeds the single-tap doesn't fire. Without // this, double-clicking a chip would briefly flash the // preview before entering rename mode. ExclusiveGesture( TapGesture(count: 2).onEnded { guard onRename != nil, !isEditing else { return } startRename(name) }, TapGesture(count: 1).onEnded { guard !isEditing else { return } onSelect(name) } ) ) } private func startRename(_ name: String) { renamingName = name renameText = name // Focus on the next runloop tick so the @FocusState observer sees // the TextField after it's been mounted into the hierarchy. DispatchQueue.main.async { renameFieldFocused = true } } private func commitRename(original: String) { let trimmed = renameText.trimmingCharacters(in: .whitespacesAndNewlines) // Always exit rename mode — validation failures silently revert // to the original name without an error banner. defer { renamingName = nil } guard !trimmed.isEmpty, trimmed != original, !names.contains(trimmed), Self.isValidSlug(trimmed) else { return } onRename?(original, trimmed) } /// Mirror of the server's profile-name slug rule /// (`omlx/model_profiles.py:validate_profile_name`). Pre-checking /// client-side avoids a doomed PUT round-trip for invalid names. private static func isValidSlug(_ s: String) -> Bool { guard let re = try? NSRegularExpression( pattern: #"^[a-z0-9][a-z0-9_-]{0,31}$"# ) else { return false } let range = NSRange(s.startIndex..., in: s) return re.firstMatch(in: s, range: range) != nil } } // MARK: - ActiveProfileBanner /// Banner that sits above the chip groups (full) or above Basic/Advanced /// (slim) summarizing what's currently attached + what unsaved work /// exists. The state-machine logic lives in the VM; this view just /// renders one of three shapes (working / named / defaults). struct ActiveProfileBanner: View { let state: ActiveProfileState let isSlim: Bool /// nil when the corresponding action isn't relevant for this state. let onUpdateBasedOn: (() -> Void)? let onSaveAsNew: (() -> Void)? let onRevert: (() -> Void)? @Environment(\.omlxTheme) private var theme var body: some View { HStack(spacing: 12) { Circle() .fill(dotColor) .frame(width: 8, height: 8) VStack(alignment: .leading, spacing: 2) { Text(titleText) .font(.omlxText(13.5, weight: .semibold)) .foregroundStyle(theme.text) subtitleView } Spacer(minLength: 8) actions } .padding(.horizontal, isSlim ? 12 : 14) .padding(.vertical, isSlim ? 10 : 12) .background(bannerBackground) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .strokeBorder(bannerBorder, lineWidth: 0.5) ) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .padding(.horizontal, 14) .padding(.bottom, 12) } private var dotColor: Color { switch state { case .working: // Working / unsaved → amber (matches HTML mock and the // "you have unsaved edits" affordance pattern). return theme.amberDot case .named(let scope, _): return ProfileScopeMeta.color(scope, theme: theme) case .defaults: return theme.textTertiary } } private var titleText: String { switch state { case .working: return String(localized: "profile.banner.working.title", defaultValue: "Working profile", comment: "Profile banner title when the user has unsaved edits") case .named(_, let name): return name case .defaults: return String(localized: "profile.banner.defaults.title", defaultValue: "No profile", comment: "Profile banner title when the model uses server defaults") } } @ViewBuilder private var subtitleView: some View { switch state { case .working(let basedOn): if let basedOn { Text(String(localized: "profile.banner.working.subtitle.based_on", defaultValue: "Unsaved · based on \(basedOn.name) (\(ProfileScopeMeta.label(basedOn.scope)))", comment: "Profile banner subtitle for working state with a base profile; placeholders are profile name and scope label")) .font(.omlxText(11.5)) .foregroundStyle(theme.textSecondary) } else { Text(String(localized: "profile.banner.working.subtitle.defaults", defaultValue: "Unsaved · based on server defaults", comment: "Profile banner subtitle when working profile has no named base")) .font(.omlxText(11.5)) .foregroundStyle(theme.textSecondary) } case .named(let scope, _): Text(String(localized: "profile.banner.named.subtitle", defaultValue: "\(ProfileScopeMeta.label(scope)) profile · active on this model", comment: "Profile banner subtitle when a named profile is active; placeholder is scope label")) .font(.omlxText(11.5)) .foregroundStyle(theme.textSecondary) case .defaults: Text(String(localized: "profile.banner.defaults.subtitle", defaultValue: "Using server defaults · edit any field to start a working profile", comment: "Profile banner subtitle when no profile is assigned")) .font(.omlxText(11.5)) .foregroundStyle(theme.textSecondary) } } @ViewBuilder private var actions: some View { HStack(spacing: 6) { switch state { case .working(let basedOn): if let basedOn, basedOn.scope != .preset, let onUpdateBasedOn { Button(String(localized: "profile.banner.action.update_based_on", defaultValue: "Update \(basedOn.name)", comment: "Profile banner action to overwrite the base profile with working edits; placeholder is the base profile's name")) { onUpdateBasedOn() } .buttonStyle(.omlx(.normal, size: .small)) } if let onSaveAsNew { Button(String(localized: "profile.banner.action.save_as_new", defaultValue: "Save as new", comment: "Profile banner action that saves working edits as a new profile")) { onSaveAsNew() } .buttonStyle(.omlx(.primary, size: .small)) } if let onRevert { Button(basedOn == nil ? String(localized: "profile.banner.action.discard", defaultValue: "Discard", comment: "Profile banner action that discards working edits when there's no base profile") : String(localized: "profile.banner.action.revert", defaultValue: "Revert", comment: "Profile banner action that reverts working edits back to the base profile")) { onRevert() } .buttonStyle(.omlx(.plain, size: .small)) } case .named, .defaults: EmptyView() } } } @ViewBuilder private var bannerBackground: some View { switch state { case .working: theme.amberDot.opacity(theme.isDark ? 0.08 : 0.07) case .named, .defaults: theme.groupBg } } private var bannerBorder: Color { switch state { case .working: return theme.amberDot.opacity(theme.isDark ? 0.28 : 0.35) case .named, .defaults: return theme.groupBorder } } } // MARK: - SaveAsPopover struct SaveAsPopover: View { @Binding var name: String @Binding var scope: ProfileScope /// Save as new only supports global / model; preset is read-only. let onCommit: () -> Void let onCancel: () -> Void @Environment(\.omlxTheme) private var theme @FocusState private var nameFocused: Bool var body: some View { HStack(spacing: 10) { Text(String(localized: "profile.save_as.title", defaultValue: "Save current profile as", comment: "Lead label inside the Save-as popover for naming a new profile")) .font(.omlxText(12, weight: .medium)) .foregroundStyle(theme.textSecondary) Segmented( selection: $scope, options: [ (.global, String(localized: "profile.scope.global", defaultValue: "Global", comment: "Scope label for user-defined global profiles")), (.model, String(localized: "profile.scope.model", defaultValue: "Model", comment: "Scope label for per-model profiles")), ] ) TextInput(text: $name, placeholder: String(localized: "profile.save_as.name.placeholder", defaultValue: "profile-name", comment: "Placeholder text inside the new-profile name field"), mono: true) .frame(maxWidth: .infinity) .focused($nameFocused) .onSubmit { onCommit() } Button(String(localized: "common.cancel", defaultValue: "Cancel", comment: "Generic Cancel button label")) { onCancel() } .buttonStyle(.omlx(.normal, size: .small)) Button(String(localized: "common.save", defaultValue: "Save", comment: "Generic Save button label")) { onCommit() } .buttonStyle(.omlx(.primary, size: .small)) .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) } .padding(12) .background(theme.groupBg) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .strokeBorder(theme.groupBorder, lineWidth: 0.5) ) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .padding(.horizontal, 14) .padding(.bottom, 12) .onAppear { nameFocused = true } } } // MARK: - ProfileDetailCard /// Compact visual summary of a profile's settings — sampling meters, /// capacity stats, penalty bars, behavior flag chips, and aliases. /// Renders below the chip groups (preview-on-click) and at the bottom /// of the Profiles tab as the read-only Server Defaults card. struct ProfileDetailCard: View { let name: String let scope: ProfileScope? // nil → defaults card (no scope dot) let settings: [String: AnyCodable] let isActive: Bool let isWorking: Bool let basedOn: ActiveProfileState.NamedProfileRef? /// True when the card is being shown for the chip the working /// profile forked from — dashed border treatment in the chip group, /// "Base of working" badge here. let isWorkingBase: Bool /// Compact mode shrinks padding; used for the Server Defaults card. let compact: Bool let hasWorking: Bool // nil means the action isn't relevant for this card. var onApply: (() -> Void)? = nil var onUpdateFromWorking: (() -> Void)? = nil var onDelete: (() -> Void)? = nil var onClosePreview: (() -> Void)? = nil /// Server `expose_as_model` state for this profile. Only meaningful /// when `onToggleExpose` is wired (model-scope profiles). var exposeAsModel: Bool = false /// Derived API model ID (`:`) shown next to /// the toggle while exposure is on. var exposedModelId: String? = nil /// Server-derived `has_engine_fields` — true when the profile carries /// engine-construction overrides, which the exposed-model overlay /// ignores. Shows a warning under the toggle while exposure is on. var hasEngineFields: Bool = false /// Non-nil renders the "Expose as model" toggle; the callback receives /// the requested state. Pass nil for templates, presets, and the /// defaults card. var onToggleExpose: ((Bool) -> Void)? = nil @Environment(\.omlxTheme) private var theme private var scopeColor: Color { guard let scope else { return theme.textTertiary } return ProfileScopeMeta.color(scope, theme: theme) } private var showApply: Bool { onApply != nil && !isActive } private var showUpdate: Bool { onUpdateFromWorking != nil && hasWorking } private var showDelete: Bool { onDelete != nil && scope != .preset && scope != nil } var body: some View { VStack(alignment: .leading, spacing: 14) { header exposeRow sections } .padding(compact ? 12 : 14) .background(theme.groupBg) .overlay( RoundedRectangle(cornerRadius: 14, style: .continuous) .strokeBorder(theme.groupBorder, lineWidth: 0.5) ) .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) .padding(.horizontal, 14) .padding(.bottom, 14) } @ViewBuilder private var header: some View { HStack(spacing: 12) { iconBox VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) { Text(name) .font(.omlxText(16, weight: .semibold)) .foregroundStyle(theme.text) if isActive && !isWorking { badge(text: String(localized: "profile.detail.badge.active", defaultValue: "ACTIVE", comment: "Badge shown on the profile detail card when this profile is active on the model"), fg: .white, bg: theme.greenDot) } if isWorking { badge(text: String(localized: "profile.detail.badge.unsaved", defaultValue: "UNSAVED", comment: "Badge on the profile detail card indicating unsaved working-state edits"), fg: Color(rgb24: 0x1A1407), bg: theme.amberDot) } if isWorkingBase && !isWorking { badge(text: String(localized: "profile.detail.badge.base_of_working", defaultValue: "BASE OF WORKING", comment: "Badge on the profile detail card marking the profile the working profile forked from"), fg: theme.textSecondary, bg: .clear, border: theme.inputBorder) } } HStack(spacing: 6) { Circle() .fill(isWorking ? theme.amberDot : scopeColor) .frame(width: 6, height: 6) Text(subtitleText) .font(.omlxText(11)) .foregroundStyle(theme.textSecondary) } } Spacer(minLength: 8) HStack(spacing: 6) { if let onClosePreview { Button(String(localized: "profile.detail.action.done", defaultValue: "Done", comment: "Profile detail card button that closes the preview overlay")) { onClosePreview() } .buttonStyle(.omlx(.plain, size: .small)) } if showUpdate, let onUpdateFromWorking { Button(String(localized: "profile.detail.action.update_with_working", defaultValue: "Update with working", comment: "Profile detail card button that writes the current working edits into this named profile")) { onUpdateFromWorking() } .buttonStyle(.omlx(.normal, size: .small)) } if showApply, let onApply { Button(String(localized: "profile.detail.action.apply", defaultValue: "Apply", comment: "Profile detail card button that activates this profile on the model")) { onApply() } .buttonStyle(.omlx(.primary, size: .small)) } if showDelete, let onDelete { Button { onDelete() } label: { Image(systemName: "trash") .font(.system(size: 12)) .foregroundStyle(theme.redDot) } .buttonStyle(.omlx(.plain, size: .small)) } } } } @ViewBuilder private var iconBox: some View { let useScopeFill = (isActive && !isWorking && scope != nil) || isWorking let fillColor: Color = isWorking ? theme.amberDot : scopeColor let symbol: String = isWorking ? "sparkles" : (scope == nil ? "gauge.medium" : "cpu") RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(useScopeFill ? LinearGradient( colors: [fillColor, fillColor.opacity(0.8)], startPoint: .top, endPoint: .bottom ) : LinearGradient( colors: [theme.codeBg, theme.codeBg], startPoint: .top, endPoint: .bottom )) .frame(width: 36, height: 36) .overlay( Image(systemName: symbol) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(useScopeFill ? .white : theme.textSecondary) ) } private var subtitleText: String { if isWorking, let basedOn { return String(localized: "profile.detail.subtitle.working_based_on", defaultValue: "Working profile · based on \(basedOn.name) (\(ProfileScopeMeta.label(basedOn.scope)))", comment: "Profile detail card subtitle in working state with a base profile; placeholders are name and scope label") } if isWorking { return String(localized: "profile.detail.subtitle.working_defaults", defaultValue: "Working profile · based on server defaults", comment: "Profile detail card subtitle in working state without a base profile") } if scope == nil { return String(localized: "profile.detail.subtitle.defaults", defaultValue: "Falls back here when no profile is set, or when a profile leaves a field empty", comment: "Profile detail card subtitle for the Server Defaults card") } let count = settings.keys.filter { !isEmptyValue(settings[$0]) }.count let scopeLabel = scope.map { ProfileScopeMeta.label($0) } ?? String(localized: "profile.detail.subtitle.scope.profile", defaultValue: "Profile", comment: "Fallback scope label in the profile detail subtitle when scope is unknown") return String(localized: "profile.detail.subtitle.named", defaultValue: "\(scopeLabel) profile · settings: \(count)", comment: "Profile detail card subtitle for a named profile; placeholders are scope label and setting count") } @ViewBuilder private var sections: some View { let s = settings let hasSampling = ["temperature", "top_p", "top_k", "min_p"].contains { s[$0] != nil } let hasCapacity = ["model_type_override", "max_context_window", "max_tokens", "ttl_seconds"] .contains { s[$0] != nil } let hasPenalty = ["repetition_penalty", "presence_penalty"].contains { s[$0] != nil } let behaviorKeys = [ "enable_thinking", "thinking_budget_enabled", "max_tool_result_tokens", "force_sampling", "is_pinned", ] let hasBehavior = behaviorKeys.contains { s[$0] != nil } // Per-model "Advanced" surface — fields from // `MODEL_SPECIFIC_PROFILE_FIELDS` in `omlx/model_profiles.py`. // None of these are universal, so they're absent from preset and // global templates; they only appear on per-model profiles. let accelKeys = [ "turboquant_kv_enabled", "dflash_enabled", "mtp_enabled", "specprefill_enabled", "index_cache_freq", "vlm_mtp_enabled", ] let hasAcceleration = accelKeys.contains { s[$0] != nil } let templateKeys = ["reasoning_parser", "chat_template_kwargs", "forced_ct_kwargs"] let hasTemplates = templateKeys.contains { k in guard let v = s[k] else { return false } return !isEmptyValue(v) } VStack(alignment: .leading, spacing: 14) { if hasSampling { samplingSection(s) } if hasCapacity { capacitySection(s) } if hasPenalty { penaltySection(s) } if hasBehavior { behaviorSection(s) } if hasAcceleration { accelerationSection(s) } if hasTemplates { templatesSection(s) } if !hasSampling && !hasCapacity && !hasPenalty && !hasBehavior && !hasAcceleration && !hasTemplates { Text(String(localized: "profile.detail.empty", defaultValue: "This profile doesn't override any settings.", comment: "Placeholder text in the profile detail card when no settings are set")) .font(.omlxText(12)) .foregroundStyle(theme.textTertiary) .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 10) } } } @ViewBuilder private func samplingSection(_ s: [String: AnyCodable]) -> some View { sectionTitle(String(localized: "profile.detail.section.sampling", defaultValue: "Sampling", comment: "Profile detail card section header for sampler-related fields")) let entries: [(String, Double?, Double, Double, String)] = [ (String(localized: "profile.detail.sampling.temperature", defaultValue: "Temperature", comment: "Sampling meter label: temperature"), doubleOf(s["temperature"]), 0, 2, "%.2f"), (String(localized: "profile.detail.sampling.top_p", defaultValue: "Top P", comment: "Sampling meter label: top-p"), doubleOf(s["top_p"]), 0, 1, "%.2f"), (String(localized: "profile.detail.sampling.top_k", defaultValue: "Top K", comment: "Sampling meter label: top-k"), doubleOf(s["top_k"]), 0, 100, "%.0f"), (String(localized: "profile.detail.sampling.min_p", defaultValue: "Min P", comment: "Sampling meter label: min-p"), doubleOf(s["min_p"]), 0, 1, "%.2f"), ].filter { $0.1 != nil } HStack(alignment: .top, spacing: 18) { ForEach(Array(entries.enumerated()), id: \.offset) { _, e in meter(label: e.0, value: e.1!, min: e.2, max: e.3, fmt: e.4) } } } @ViewBuilder private func capacitySection(_ s: [String: AnyCodable]) -> some View { sectionTitle(String(localized: "profile.detail.section.capacity", defaultValue: "Capacity", comment: "Profile detail card section header for capacity-related fields")) let entries: [(String, String)] = [ (String(localized: "profile.detail.capacity.type", defaultValue: "Type", comment: "Capacity stat label: model type override"), s["model_type_override"].flatMap { ($0.value as? String) } .flatMap { capacityType(value: $0) }), (String(localized: "profile.detail.capacity.context", defaultValue: "Context", comment: "Capacity stat label: max context window"), s["max_context_window"].flatMap { intOf($0) } .map { String(localized: "profile.detail.capacity.tokens", defaultValue: "\(fmtCtx(Double($0))) tk", comment: "Token count rendered next to a Capacity stat; placeholder is the formatted count") }), (String(localized: "profile.detail.capacity.max_tokens", defaultValue: "Max Tokens", comment: "Capacity stat label: max output tokens"), s["max_tokens"].flatMap { intOf($0) } .map { String(localized: "profile.detail.capacity.tokens.raw", defaultValue: "\($0) tk", comment: "Raw token count for max tokens; placeholder is the integer count") }), (String(localized: "profile.detail.capacity.ttl", defaultValue: "TTL", comment: "Capacity stat label: time-to-live for the model in memory"), s["ttl_seconds"].flatMap { intOf($0) } .map { $0 == 0 ? String(localized: "profile.detail.capacity.ttl.persistent", defaultValue: "Persistent", comment: "TTL value rendered when the model is pinned indefinitely") : String(localized: "profile.detail.capacity.ttl.seconds", defaultValue: "\($0)s", comment: "TTL value in seconds; placeholder is the integer seconds") }), ].compactMap { (label, value) in value.map { (label, $0) } } HStack(alignment: .top, spacing: 18) { ForEach(Array(entries.enumerated()), id: \.offset) { _, e in stat(label: e.0, value: e.1) } } } @ViewBuilder private func penaltySection(_ s: [String: AnyCodable]) -> some View { sectionTitle(String(localized: "profile.detail.section.penalties", defaultValue: "Penalties", comment: "Profile detail card section header for repetition/presence penalty fields")) let entries: [(String, Double?, Double, Double)] = [ (String(localized: "profile.detail.penalty.repetition", defaultValue: "Repetition", comment: "Penalty meter label: repetition"), doubleOf(s["repetition_penalty"]), 0, 2), (String(localized: "profile.detail.penalty.presence", defaultValue: "Presence", comment: "Penalty meter label: presence"), doubleOf(s["presence_penalty"]), -2, 2), ].filter { $0.1 != nil } HStack(alignment: .top, spacing: 18) { ForEach(Array(entries.enumerated()), id: \.offset) { _, e in meter(label: e.0, value: e.1!, min: e.2, max: e.3, fmt: "%.2f") } } } @ViewBuilder private func behaviorSection(_ s: [String: AnyCodable]) -> some View { sectionTitle(String(localized: "profile.detail.section.behavior", defaultValue: "Behavior", comment: "Profile detail card section header for behavior-flag chips")) FlowLayout(spacing: 6) { if let v = s["enable_thinking"].flatMap({ boolOf($0) }) { flagChip(label: String(localized: "profile.detail.behavior.thinking", defaultValue: "Thinking", comment: "Behavior chip: extended-thinking flag"), on: v) } if let v = s["thinking_budget_enabled"].flatMap({ boolOf($0) }) { let n = intOf(s["thinking_budget_tokens"]) ?? 8192 flagChip(label: v ? String(localized: "profile.detail.behavior.thinking_budget.on", defaultValue: "Budget · \(fmtCtx(Double(n))) tk", comment: "Behavior chip when thinking budget is enabled; placeholder is the formatted token count") : String(localized: "profile.detail.behavior.thinking_budget.off", defaultValue: "Thinking budget", comment: "Behavior chip label when thinking budget is disabled"), on: v) } if let v = s["max_tool_result_tokens"].flatMap({ intOf($0) }) { flagChip(label: String(localized: "profile.detail.behavior.limit_tool_output", defaultValue: "Limit tool output", comment: "Behavior chip: cap on tool result tokens"), on: v > 0) } if let v = s["force_sampling"].flatMap({ boolOf($0) }) { flagChip(label: String(localized: "profile.detail.behavior.force_sampling", defaultValue: "Force sampling", comment: "Behavior chip: force sampling flag"), on: v) } if let v = s["is_pinned"].flatMap({ boolOf($0) }) { flagChip(label: String(localized: "profile.detail.behavior.pinned", defaultValue: "Pinned in memory", comment: "Behavior chip: pin-in-memory flag"), on: v) } } } /// Acceleration / experimental knobs that only land on per-model /// profiles (TurboQuant KV, DFlash + drafter quant, native MTP, /// SpecPrefill, IndexCache). Renders one chip per feature so the /// preview surfaces whether a profile flips them — and at what /// settings — without forcing the user back into the Advanced tab. @ViewBuilder private func accelerationSection(_ s: [String: AnyCodable]) -> some View { sectionTitle(String(localized: "profile.detail.section.acceleration", defaultValue: "Acceleration", comment: "Profile detail card section header for experimental acceleration knobs")) FlowLayout(spacing: 6) { if let on = s["turboquant_kv_enabled"].flatMap({ boolOf($0) }) { flagChip(label: turboquantLabel(s, on: on), on: on) } if let on = s["dflash_enabled"].flatMap({ boolOf($0) }) { flagChip(label: dflashLabel(s, on: on), on: on) if on, let q = s["dflash_draft_quant_enabled"].flatMap({ boolOf($0) }), q { flagChip(label: dflashQuantLabel(s), on: true) } } if let on = s["mtp_enabled"].flatMap({ boolOf($0) }) { flagChip(label: String(localized: "profile.detail.acceleration.mtp", defaultValue: "Lightning MTP", comment: "Acceleration chip: built-in MTP head multi-token prediction"), on: on) } if let on = s["vlm_mtp_enabled"].flatMap({ boolOf($0) }) { flagChip(label: String(localized: "profile.detail.acceleration.vlm_mtp", defaultValue: "VLM MTP", comment: "Acceleration chip: VLM multi-token prediction"), on: on) } if let on = s["specprefill_enabled"].flatMap({ boolOf($0) }) { flagChip(label: specprefillLabel(s, on: on), on: on) } if let freq = intOf(s["index_cache_freq"]), freq > 0 { flagChip(label: String(localized: "profile.detail.acceleration.index_cache", defaultValue: "IndexCache · every \(freq)", comment: "Acceleration chip describing IndexCache frequency; placeholder is the integer frequency"), on: true) } } } /// Tokenizer / chat-template overrides — universal fields that the /// existing Sampling / Capacity / Penalty / Behavior sections don't /// cover. Kept separate so the visual weight matches its semantic /// distance from the sampler knobs above. @ViewBuilder private func templatesSection(_ s: [String: AnyCodable]) -> some View { sectionTitle(String(localized: "profile.detail.section.templates", defaultValue: "Templates", comment: "Profile detail card section header for tokenizer/chat-template fields")) FlowLayout(spacing: 6) { if let parser = (s["reasoning_parser"]?.value as? String)? .trimmingCharacters(in: .whitespaces), !parser.isEmpty { flagChip(label: String(localized: "profile.detail.templates.reasoning_parser", defaultValue: "Reasoning · \(parser)", comment: "Templates chip naming the active reasoning parser; placeholder is the parser identifier"), on: true) } if let count = nonEmptyKwargCount(s["chat_template_kwargs"]) { flagChip( label: String(localized: "profile.detail.templates.chat_template", defaultValue: "Chat template · overrides: \(count)", comment: "Templates chip describing chat-template kwarg override count; placeholder is the count"), on: true ) } if let count = nonEmptyKwargCount(s["forced_ct_kwargs"]) { flagChip( label: String(localized: "profile.detail.templates.forced_ct", defaultValue: "Forced CT · keys: \(count)", comment: "Templates chip describing forced chat-template key count; placeholder is the count"), on: true ) } } } private func turboquantLabel(_ s: [String: AnyCodable], on: Bool) -> String { let baseName = String(localized: "profile.detail.acceleration.turboquant", defaultValue: "TurboQuant KV", comment: "Acceleration chip base name for TurboQuant KV-cache compression") guard on else { return baseName } let bits = doubleOf(s["turboquant_kv_bits"]) let skip = intOf(s["turboquant_skip_last"]) ?? 0 let bitsText = bits.map { v in v.truncatingRemainder(dividingBy: 1) == 0 ? String(localized: "profile.detail.acceleration.turboquant.bits_int", defaultValue: "\(Int(v))bit", comment: "TurboQuant bit-width suffix when the value is an integer; placeholder is the int") : String(format: String(localized: "profile.detail.acceleration.turboquant.bits_decimal", defaultValue: "%.1fbit", comment: "TurboQuant bit-width suffix when fractional; %.1f gets the decimal width"), v) } let skipText = skip > 0 ? String(localized: "profile.detail.acceleration.turboquant.skip", defaultValue: "skip \(skip)", comment: "TurboQuant skip-last-N suffix; placeholder is the layer count") : nil let parts = [bitsText, skipText].compactMap { $0 } return parts.isEmpty ? baseName : String(localized: "profile.detail.acceleration.turboquant.with_parts", defaultValue: "TurboQuant KV · \(parts.joined(separator: " / "))", comment: "TurboQuant chip with detail suffix; placeholder is the joined detail parts") } private func dflashLabel(_ s: [String: AnyCodable], on: Bool) -> String { let baseName = String(localized: "profile.detail.acceleration.dflash", defaultValue: "DFlash", comment: "Acceleration chip base name for DFlash drafter") guard on else { return baseName } let drafter = (s["dflash_draft_model"]?.value as? String)? .trimmingCharacters(in: .whitespaces) if let d = drafter, !d.isEmpty { return String(localized: "profile.detail.acceleration.dflash.with_drafter", defaultValue: "DFlash · \(d)", comment: "DFlash chip with drafter model id; placeholder is the model identifier") } return baseName } private func dflashQuantLabel(_ s: [String: AnyCodable]) -> String { let w = intOf(s["dflash_draft_quant_weight_bits"]) ?? 4 let a = intOf(s["dflash_draft_quant_activation_bits"]) ?? 8 let g = intOf(s["dflash_draft_quant_group_size"]) ?? 64 return String(localized: "profile.detail.acceleration.dflash_quant", defaultValue: "Drafter quant · W\(w)/A\(a) G\(g)", comment: "DFlash drafter quantization chip; placeholders are weight bits, activation bits, group size") } private func specprefillLabel(_ s: [String: AnyCodable], on: Bool) -> String { let baseName = String(localized: "profile.detail.acceleration.specprefill", defaultValue: "SpecPrefill", comment: "Acceleration chip base name for SpecPrefill") guard on else { return baseName } // `specprefill_keep_pct` is a fraction (0.1–0.5 per // `omlx/model_settings.py:62`); render as a percent so the chip // reads naturally. let keep = doubleOf(s["specprefill_keep_pct"]) .map { String(format: "%.0f%%", $0 * 100) } let thresh = intOf(s["specprefill_threshold"]) .map { String(localized: "profile.detail.acceleration.specprefill.threshold", defaultValue: "≥\(fmtCtx(Double($0))) tk", comment: "SpecPrefill threshold suffix; placeholder is the formatted token count") } let parts = [keep, thresh].compactMap { $0 } return parts.isEmpty ? baseName : String(localized: "profile.detail.acceleration.specprefill.with_parts", defaultValue: "SpecPrefill · \(parts.joined(separator: " / "))", comment: "SpecPrefill chip with detail suffix; placeholder is the joined detail parts") } /// Count of non-empty key-value pairs in a `[String: AnyCodable]` /// dictionary blob. Returns nil if the field is absent, empty, or /// not a dict — those cases should hide the chip entirely. private func nonEmptyKwargCount(_ v: AnyCodable?) -> Int? { guard let v else { return nil } if let dict = v.value as? [String: AnyCodable] { return dict.isEmpty ? nil : dict.count } if let dict = v.value as? [String: Any] { return dict.isEmpty ? nil : dict.count } return nil } /// "Expose as model" toggle — mirrors the web dashboard's checkbox on /// per-model profiles. While on, the profile serves as its own model /// ID on /v1/models, overlaying its settings on the base model. @ViewBuilder private var exposeRow: some View { if let onToggleExpose { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Toggle(isOn: Binding( get: { exposeAsModel }, set: { onToggleExpose($0) } )) { Text(String(localized: "profile.detail.expose_as_model", defaultValue: "Expose as model", comment: "Toggle on the profile detail card that publishes the profile as its own model ID")) .font(.omlxText(11.5, weight: .medium)) .foregroundStyle(theme.textSecondary) } .toggleStyle(.switch) .controlSize(.mini) if exposeAsModel, let exposedModelId { Text(exposedModelId) .font(.omlxMono(11)) .foregroundStyle(theme.textTertiary) .lineLimit(1) .truncationMode(.middle) } Spacer(minLength: 0) } .help(String(localized: "profile.detail.expose_as_model.help", defaultValue: "Serve this profile as its own model ID on /v1/models, sharing the base model's engine", comment: "Tooltip on the expose-as-model toggle")) if exposeAsModel && hasEngineFields { Text(String(localized: "profile.detail.expose_as_model.engine_fields_hint", defaultValue: "Engine-level settings in this profile only take effect when it is applied to the base model — they don't change the exposed model.", comment: "Warning under the expose-as-model toggle when the profile carries engine-construction settings")) .font(.omlxText(10.5)) .foregroundStyle(theme.amberDot) .fixedSize(horizontal: false, vertical: true) } } } } @ViewBuilder private func sectionTitle(_ s: String) -> some View { Text(s) .font(.omlxText(10, weight: .heavy)) .kerning(0.9) .textCase(.uppercase) .foregroundStyle(theme.textTertiary) .padding(.bottom, 2) } @ViewBuilder private func meter(label: String, value: Double, min minV: Double, max maxV: Double, fmt: String) -> some View { VStack(alignment: .leading, spacing: 5) { HStack(alignment: .firstTextBaseline) { Text(label) .font(.omlxText(10.5, weight: .heavy)) .kerning(0.6) .textCase(.uppercase) .foregroundStyle(theme.textSecondary) Spacer(minLength: 4) Text(String(format: fmt, value)) .font(.omlxMono(12.5, weight: .semibold)) .foregroundStyle(theme.text) } GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 2, style: .continuous) .fill(theme.text.opacity(theme.isDark ? 0.07 : 0.06)) let raw = (value - minV) / Swift.max(0.001, maxV - minV) let pct = Swift.max(0, Swift.min(1, raw)) RoundedRectangle(cornerRadius: 2, style: .continuous) .fill(theme.accent) .frame(width: geo.size.width * pct) } } .frame(height: 4) } .frame(maxWidth: .infinity, alignment: .leading) } @ViewBuilder private func stat(label: String, value: String) -> some View { VStack(alignment: .leading, spacing: 4) { Text(label) .font(.omlxText(10.5, weight: .heavy)) .kerning(0.6) .textCase(.uppercase) .foregroundStyle(theme.textSecondary) Text(value) .font(.omlxMono(14, weight: .semibold)) .foregroundStyle(theme.text) .lineLimit(1) .truncationMode(.tail) } .frame(maxWidth: .infinity, alignment: .leading) } @ViewBuilder private func flagChip(label: String, on: Bool) -> some View { HStack(spacing: 6) { Circle() .fill(on ? theme.greenDot : theme.text.opacity(0.18)) .frame(width: 6, height: 6) Text(label) .font(.omlxText(11.5, weight: .medium)) .foregroundStyle(on ? theme.greenDot : theme.textSecondary) } .padding(.horizontal, 9) .frame(height: 24) .background( Capsule().fill( on ? theme.greenDot.opacity(theme.isDark ? 0.16 : 0.14) : theme.text.opacity(theme.isDark ? 0.04 : 0.035) ) ) .overlay( Capsule().strokeBorder( on ? theme.greenDot.opacity(theme.isDark ? 0.25 : 0.3) : theme.inputBorder, lineWidth: 0.5 ) ) } @ViewBuilder private func badge(text: String, fg: Color, bg: Color, border: Color? = nil) -> some View { Text(text) .font(.omlxText(9.5, weight: .heavy)) .kerning(0.7) .foregroundStyle(fg) .padding(.horizontal, 6).padding(.vertical, 2) .background(bg) .overlay( RoundedRectangle(cornerRadius: 4, style: .continuous) .strokeBorder(border ?? .clear, lineWidth: border == nil ? 0 : 0.5) ) .clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous)) } } // MARK: - Helpers (settings dict reads) private func doubleOf(_ v: AnyCodable?) -> Double? { guard let v else { return nil } if let d = v.value as? Double { return d } if let i = v.value as? Int { return Double(i) } return nil } private func intOf(_ v: AnyCodable?) -> Int? { guard let v else { return nil } if let i = v.value as? Int { return i } if let d = v.value as? Double { return Int(d) } return nil } private func boolOf(_ v: AnyCodable?) -> Bool? { guard let v else { return nil } return v.value as? Bool } private func capacityType(value: String) -> String? { let v = value.trimmingCharacters(in: .whitespaces) return v.isEmpty ? nil : v } private func fmtCtx(_ v: Double) -> String { if v >= 1_000_000 { let q = v / 1_000_000 return q.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0fM", q) : String(format: "%.1fM", q) } if v >= 1_000 { return String(format: "%.0fK", v / 1_000) } return String(Int(v)) } private func isEmptyValue(_ v: AnyCodable?) -> Bool { guard let v else { return true } if v.value is NSNull { return true } if let s = v.value as? String, s.isEmpty { return true } if let arr = v.value as? [AnyCodable], arr.isEmpty { return true } if let dict = v.value as? [String: AnyCodable], dict.isEmpty { return true } return false } // MARK: - FlowLayout (chip wrapping) /// Wrapping chip layout. `arrange` is the single source of truth for /// both passes (`sizeThatFits` and `placeSubviews`) so the reported size /// and the actual placement always agree — even when SwiftUI calls the /// sizing pass with an unspecified proposal before the real bounds are /// known. struct FlowLayout: Layout { let spacing: CGFloat init(spacing: CGFloat = 6) { self.spacing = spacing } func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let containerWidth = proposal.width ?? .greatestFiniteMagnitude return arrange(containerWidth: containerWidth, subviews: subviews).size } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let result = arrange(containerWidth: bounds.width, subviews: subviews) for (i, frame) in result.frames.enumerated() { subviews[i].place( at: CGPoint(x: bounds.minX + frame.origin.x, y: bounds.minY + frame.origin.y), proposal: ProposedViewSize(frame.size) ) } } private struct Arrangement { let size: CGSize let frames: [CGRect] } private func arrange(containerWidth: CGFloat, subviews: Subviews) -> Arrangement { var frames: [CGRect] = [] frames.reserveCapacity(subviews.count) var x: CGFloat = 0 var y: CGFloat = 0 var rowH: CGFloat = 0 var maxRowEnd: CGFloat = 0 for sub in subviews { let size = sub.sizeThatFits(.unspecified) // Wrap when this chip doesn't fit *and* there's at least one // chip already on the current row. The leading-edge check // prevents an infinite-width single chip from being thrown // off the layout. if x > 0 && x + size.width > containerWidth { y += rowH + spacing x = 0 rowH = 0 } frames.append(CGRect(origin: CGPoint(x: x, y: y), size: size)) x += size.width + spacing rowH = max(rowH, size.height) maxRowEnd = max(maxRowEnd, x - spacing) } return Arrangement( size: CGSize(width: maxRowEnd, height: y + rowH), frames: frames ) } }