diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..795230a --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,132 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-06-24T04:56:51Z", + "title": "Design System: Core AI Lab", + "extensions": { + "colorMeta": { + "system-blue": { + "role": "primary", + "displayName": "System Blue", + "tonalRamp": ["#EAF3FF", "#8FC2FF", "#007AFF", "#0057B8", "#003A7A"] + }, + "system-green": { + "role": "success", + "displayName": "System Green", + "tonalRamp": ["#EAF9EE", "#91E3A5", "#34C759", "#248A3D", "#176128"] + }, + "system-orange": { + "role": "warning", + "displayName": "System Orange", + "tonalRamp": ["#FFF4E5", "#FFD08A", "#FF9500", "#B56900", "#7A4700"] + }, + "system-red": { + "role": "destructive", + "displayName": "System Red", + "tonalRamp": ["#FFECEC", "#FF9C99", "#FF3B30", "#C5221F", "#7F1715"] + }, + "on-accent": { + "role": "on-primary", + "displayName": "On Accent" + } + }, + "typographyMeta": { + "title": { + "displayName": "System Title", + "purpose": "Navigation and workspace identity; use SwiftUI semantic title styles at runtime." + }, + "body": { + "displayName": "System Body", + "purpose": "Controls, instructions, results, and user-authored content." + }, + "technical": { + "displayName": "Technical Mono", + "purpose": "Commands, hashes, paths, identifiers, shapes, and numeric evidence." + } + }, + "shadows": [], + "motion": [ + { + "name": "system-state-change", + "value": "platform-default", + "purpose": "Use SwiftUI's restrained state transitions and honor Reduce Motion." + } + ], + "breakpoints": [] + }, + "components": [ + { + "name": "Primary Action", + "kind": "button", + "refersTo": "button-primary", + "description": "The single prominent action in a workflow, mirroring SwiftUI borderedProminent styling.", + "html": "", + "css": ".ds-primary-action { appearance: none; border: 0; border-radius: 8px; padding: 8px 14px; background: #007AFF; color: #FFFFFF; font: 600 14px/1.25 -apple-system, BlinkMacSystemFont, sans-serif; transition: filter 140ms ease, transform 140ms ease; } .ds-primary-action:hover { filter: brightness(1.06); } .ds-primary-action:active { transform: scale(0.98); } .ds-primary-action:focus-visible { outline: 3px solid rgba(0,122,255,0.35); outline-offset: 2px; }" + }, + { + "name": "Secondary Action", + "kind": "button", + "description": "A supporting action with standard system prominence.", + "html": "", + "css": ".ds-secondary-action { appearance: none; border: 1px solid rgba(60,60,67,0.24); border-radius: 8px; padding: 7px 13px; background: rgba(118,118,128,0.12); color: #1D1D1F; font: 500 14px/1.25 -apple-system, BlinkMacSystemFont, sans-serif; transition: background 140ms ease, transform 140ms ease; } .ds-secondary-action:hover { background: rgba(118,118,128,0.18); } .ds-secondary-action:active { transform: scale(0.98); } .ds-secondary-action:focus-visible { outline: 3px solid rgba(0,122,255,0.35); outline-offset: 2px; }" + }, + { + "name": "Search Field", + "kind": "input", + "description": "Native-feeling search for libraries and registries.", + "html": "", + "css": ".ds-search { display: inline-flex; align-items: center; gap: 7px; width: 280px; border-radius: 8px; padding: 7px 10px; background: rgba(118,118,128,0.12); color: rgba(60,60,67,0.65); font: 400 14px/1.25 -apple-system, BlinkMacSystemFont, sans-serif; } .ds-search input { min-width: 0; flex: 1; border: 0; outline: 0; background: transparent; color: #1D1D1F; font: inherit; } .ds-search:focus-within { box-shadow: 0 0 0 3px rgba(0,122,255,0.28); }" + }, + { + "name": "Sidebar Destination", + "kind": "nav", + "description": "A concise system-symbol destination row with a selected state.", + "html": "", + "css": ".ds-sidebar { display: grid; gap: 2px; width: 240px; font: 500 14px/1.25 -apple-system, BlinkMacSystemFont, sans-serif; } .ds-sidebar-row { display: flex; align-items: center; gap: 8px; border: 0; border-radius: 7px; padding: 7px 9px; background: transparent; color: #1D1D1F; text-align: left; } .ds-sidebar-row:hover { background: rgba(118,118,128,0.10); } .ds-sidebar-row.ds-selected { background: #007AFF; color: #FFFFFF; } .ds-sidebar-row:focus-visible { outline: 3px solid rgba(0,122,255,0.35); outline-offset: 1px; }" + }, + { + "name": "Evidence Section", + "kind": "custom", + "description": "A grouped, flat evidence surface using labels and selectable technical values.", + "html": "

Integrity

SHA-256
0f4e…a819
Stored size
428 MB
", + "css": ".ds-evidence { width: 360px; color: #1D1D1F; font: 400 14px/1.35 -apple-system, BlinkMacSystemFont, sans-serif; } .ds-evidence h3 { margin: 0 0 7px; color: rgba(60,60,67,0.72); font-size: 12px; font-weight: 600; text-transform: uppercase; } .ds-evidence dl { margin: 0; overflow: hidden; border: 1px solid rgba(60,60,67,0.16); border-radius: 10px; background: rgba(255,255,255,0.9); } .ds-evidence dl div { display: flex; justify-content: space-between; gap: 20px; padding: 10px 12px; } .ds-evidence dl div + div { border-top: 1px solid rgba(60,60,67,0.12); } .ds-evidence dt { color: rgba(60,60,67,0.72); } .ds-evidence dd { margin: 0; font-family: ui-monospace, SFMono-Regular, monospace; }" + } + ], + "narrative": { + "northStar": "The Native Instrument", + "overview": "Core AI Lab is calm at rest, exact when interrogated, and candid about what it knows. It uses standard platform structure so technical evidence—not ornamental interface chrome—holds attention. Every workspace moves from orientation through preparation and action to observable, verifiable results.", + "keyCharacteristics": [ + "Native and platform-adaptive", + "Evidence-led rather than decorative", + "Progressive disclosure for technical density", + "Explicit prerequisites, progress, cancellation, and failure", + "Keyboard and VoiceOver accessible" + ], + "rules": [ + { + "name": "The One Accent Rule", + "body": "Use one prominent blue action per workflow; supporting actions retain standard prominence.", + "section": "colors" + }, + { + "name": "The Semantic Type Rule", + "body": "Use SwiftUI semantic styles and let Dynamic Type determine runtime size.", + "section": "typography" + }, + { + "name": "The Platform Depth Rule", + "body": "If SwiftUI already communicates the layer, do not add another shadow, blur, stroke, or floating container.", + "section": "elevation" + } + ], + "dos": [ + "Do start each workspace with purpose, current state, and the next meaningful action.", + "Do keep evidence precise, selectable, and visually subordinate to the current task.", + "Do pair status color with text and a familiar symbol." + ], + "donts": [ + "Don't build generic card grids or prompt-first chat surfaces.", + "Don't add ornamental glass, neon gradients, or unexplained status lights.", + "Don't claim execution placement, performance, or cache benefit without evidence." + ] + } +} diff --git a/CoreAIFrameworkLab.xcodeproj/project.pbxproj b/CoreAIFrameworkLab.xcodeproj/project.pbxproj index bfb771d..ad52eda 100644 --- a/CoreAIFrameworkLab.xcodeproj/project.pbxproj +++ b/CoreAIFrameworkLab.xcodeproj/project.pbxproj @@ -158,6 +158,7 @@ 3E682C6B70D19395DAFE10A1 /* CoreAIFunctionValueKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB04F943311AA32BBCD7E95D /* CoreAIFunctionValueKind.swift */; }; 3E7EA385F765D82D1650E92C /* CoreAIRuntimeMetricEvidence.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0877E38482D768EC4F68D71 /* CoreAIRuntimeMetricEvidence.swift */; }; 3F0C34F96B21DE35BE1E9A04 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7FB5157DD183D55DA2447D /* ContentView.swift */; }; + 8FCF2AB32FEBA26500094256 /* CoreAIWorkspaceInspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FCF2AB22FEBA26500094256 /* CoreAIWorkspaceInspectorView.swift */; }; 3FBBF65C7E3750853D7CA39C /* CoreAISwiftPackageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18EDE7C13B56DD6321F3DFE0 /* CoreAISwiftPackageGenerator.swift */; }; 3F78820211A431FF3EC16D78 /* CoreAIProjectSpecializationCacheView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A6790D1F0EFB21CEDC33D /* CoreAIProjectSpecializationCacheView.swift */; }; 405EB3C4D141B116DE4EF121 /* CoreAIFunctionRunResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CCDF520342FD26442812A9 /* CoreAIFunctionRunResult.swift */; }; @@ -591,6 +592,7 @@ 0E0BDACAD345692BD0CF2CD0 /* CoreAINECompatibilityCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreAINECompatibilityCheck.swift; sourceTree = ""; }; 0E13A77B8F8BA388C8EA12A9 /* CoreAIProjectRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreAIProjectRoute.swift; sourceTree = ""; }; 0E7FB5157DD183D55DA2447D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 8FCF2AB22FEBA26500094256 /* CoreAIWorkspaceInspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreAIWorkspaceInspectorView.swift; sourceTree = ""; }; 0EA900D41D26D5440F5B873D /* AppleModelLibraryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleModelLibraryModel.swift; sourceTree = ""; }; 0F3153C86C4BB1C0F56A8514 /* CoreAIRuntimeLifecycleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreAIRuntimeLifecycleView.swift; sourceTree = ""; }; 0F5B304A4D84B06BFE71C5A8 /* CoreAIPipelineCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreAIPipelineCodec.swift; sourceTree = ""; }; @@ -1580,6 +1582,7 @@ isa = PBXGroup; children = ( 0E7FB5157DD183D55DA2447D /* ContentView.swift */, + 8FCF2AB22FEBA26500094256 /* CoreAIWorkspaceInspectorView.swift */, 7716B246D5E600F1622AB1D5 /* Core AI Lab.icon */, 21EF809F4325A5DED84D6974 /* CoreAILabApp.swift */, 3865FA28608466F39B8B3251 /* CoreAILabSection.swift */, @@ -2143,6 +2146,7 @@ 27368950A09746EC0B06C643 /* ChatterboxWorkspaceModel.swift in Sources */, 862DF4D00DCD7711215B7B2F /* ChatterboxWorkspaceView.swift in Sources */, 3F0C34F96B21DE35BE1E9A04 /* ContentView.swift in Sources */, + 8FCF2AB32FEBA26500094256 /* CoreAIWorkspaceInspectorView.swift in Sources */, D236381C42FD8790C405E461 /* CoreAIAheadOfTimeCompileScriptGenerator.swift in Sources */, 3CF411188EC9E73089394377 /* CoreAIArtifactDigest.swift in Sources */, 5F9C403770360C83B2236A22 /* CoreAIArtifactDigesting.swift in Sources */, diff --git a/CoreAILab/ContentView.swift b/CoreAILab/ContentView.swift index 849202f..1cc4249 100644 --- a/CoreAILab/ContentView.swift +++ b/CoreAILab/ContentView.swift @@ -2,31 +2,64 @@ import SwiftData import SwiftUI struct ContentView: View { - @State private var selection: CoreAILabSection? = .projects + @SceneStorage("CoreAILab.selectedSection") + private var selectedSectionRawValue = CoreAILabSection.projects.rawValue + + @SceneStorage("CoreAILab.isWorkspaceInspectorPresented") + private var isWorkspaceInspectorPresented = false + + @State private var columnVisibility: NavigationSplitViewVisibility = .all var body: some View { - NavigationSplitView { - List(selection: $selection) { - Section("Workspaces") { - ForEach(CoreAILabSection.workspaces) { section in + NavigationSplitView(columnVisibility: $columnVisibility) { + List(selection: selectedSectionBinding) { + Section("Library") { + ForEach(CoreAILabSection.library) { section in + NavigationLink(value: section) { + Label(section.title, systemImage: section.systemImage) + } + .help(section.summary) + .accessibilityHint(section.summary) + } + } + + Section("Build") { + ForEach(CoreAILabSection.build) { section in NavigationLink(value: section) { Label(section.title, systemImage: section.systemImage) } + .help(section.summary) + .accessibilityHint(section.summary) } } - Section("Tools") { - ForEach(CoreAILabSection.tools) { section in + Section("Run") { + ForEach(CoreAILabSection.run) { section in NavigationLink(value: section) { Label(section.title, systemImage: section.systemImage) } + .help(section.summary) + .accessibilityHint(section.summary) + } + } + + Section("Validate") { + ForEach(CoreAILabSection.validate) { section in + NavigationLink(value: section) { + Label(section.title, systemImage: section.systemImage) + } + .help(section.summary) + .accessibilityHint(section.summary) } } } - .navigationTitle("Core AI Lab") - .navigationSplitViewColumnWidth(min: 190, ideal: 220, max: 280) + .listStyle(.sidebar) + .navigationSplitViewColumnWidth(min: 200, ideal: 240, max: 280) +#if os(macOS) + .toolbar(removing: .sidebarToggle) +#endif } detail: { - switch selection ?? .projects { + switch selectedSection { case .projects: CoreAIProjectLibraryView() case .appleModels: @@ -54,6 +87,60 @@ struct ContentView: View { } } .navigationSplitViewStyle(.prominentDetail) + .formStyle(.grouped) + .tint(.blue) + .inspector(isPresented: $isWorkspaceInspectorPresented) { + CoreAIWorkspaceInspectorView(section: selectedSection) + } + .toolbar { +#if os(macOS) + ToolbarItem(placement: .navigation) { + Button( + "Toggle Sidebar", + systemImage: "sidebar.leading", + action: toggleSidebar + ) + .help("Show or hide the workspace sidebar") + .keyboardShortcut("s", modifiers: [.command, .control]) + } +#endif + + ToolbarItem(placement: .primaryAction) { + Button( + "Workspace Inspector", + systemImage: "sidebar.trailing", + action: toggleWorkspaceInspector + ) + .help("Show the selected workspace's workflow and evidence boundary") +#if os(macOS) + .keyboardShortcut("0", modifiers: [.command, .option]) +#endif + } + } +#if os(macOS) + .frame(minWidth: 1_000, minHeight: 680) +#endif + } + + private func toggleWorkspaceInspector() { + isWorkspaceInspectorPresented.toggle() + } + + private var selectedSection: CoreAILabSection { + CoreAILabSection(rawValue: selectedSectionRawValue) ?? .projects + } + + private var selectedSectionBinding: Binding { + Binding( + get: { selectedSection }, + set: { selectedSectionRawValue = ($0 ?? .projects).rawValue } + ) + } + + private func toggleSidebar() { + withAnimation { + columnVisibility = columnVisibility == .detailOnly ? .all : .detailOnly + } } } diff --git a/CoreAILab/CoreAILabApp.swift b/CoreAILab/CoreAILabApp.swift index 12deea5..b7ceac5 100644 --- a/CoreAILab/CoreAILabApp.swift +++ b/CoreAILab/CoreAILabApp.swift @@ -19,5 +19,8 @@ struct CoreAILabApp: App { ContentView() } .modelContainer(modelContainer) +#if os(macOS) + .defaultSize(width: 1_280, height: 820) +#endif } } diff --git a/CoreAILab/CoreAILabSection.swift b/CoreAILab/CoreAILabSection.swift index 3cc9742..85e4270 100644 --- a/CoreAILab/CoreAILabSection.swift +++ b/CoreAILab/CoreAILabSection.swift @@ -12,8 +12,10 @@ enum CoreAILabSection: String, CaseIterable, Hashable, Identifiable { case runtime case deviceLab - static let tools: [Self] = [.assetInspector, .runtime, .deviceLab] - static let workspaces = allCases.filter { !tools.contains($0) } + static let library: [Self] = [.projects, .appleModels, .recipes] + static let build: [Self] = [.conversion, .recipeStudio] + static let run: [Self] = [.chatterbox, .diarization, .runtime] + static let validate: [Self] = [.assetInspector, .deviceLab] var id: Self { self } @@ -66,4 +68,132 @@ enum CoreAILabSection: String, CaseIterable, Hashable, Identifiable { "iphone.gen3" } } + + var summary: String { + switch self { + case .projects: + "Organize imported artifacts, provenance, runs, and evidence." + case .appleModels: + "Browse Apple's pinned Core AI export recipes." + case .recipes: + "Review curated recipes and inspect imported bundles." + case .conversion: + "Export a Core AI model from an Apple recipe." + case .recipeStudio: + "Author and validate recipe and pipeline contracts." + case .chatterbox: + "Generate speech with the bundled Core AI pipeline." + case .diarization: + "Build an anonymous speaker timeline from local media." + case .assetInspector: + "Inspect functions, compute types, and specialization caches." + case .runtime: + "Run task adapters and record evidence-backed timing." + case .deviceLab: + "Plan iPhone delivery and import physical-device evidence." + } + } + + var areaTitle: String { + switch self { + case .projects, .appleModels, .recipes: + "Library" + case .conversion, .recipeStudio: + "Build" + case .chatterbox, .diarization, .runtime: + "Run" + case .assetInspector, .deviceLab: + "Validate" + } + } + + var workflowSteps: [String] { + switch self { + case .projects: + [ + "Create or open a project", + "Import checked artifacts", + "Review runs and evidence" + ] + case .appleModels: + [ + "Choose an Apple recipe", + "Review its requirements and provenance", + "Convert it or open its runtime" + ] + case .recipes: + [ + "Choose a curated recipe", + "Review its code and provenance", + "Import the approved bundle" + ] + case .conversion: + [ + "Configure the recipe", + "Validate the local environment", + "Convert and verify the artifacts" + ] + case .recipeStudio: + [ + "Define the source and contracts", + "Resolve unsupported operations", + "Compose and validate the pipeline" + ] + case .chatterbox: + [ + "Prepare the bundled models", + "Write expressive speech", + "Generate and review local audio" + ] + case .diarization: + [ + "Import local media", + "Analyze anonymous speakers", + "Review the speaker timeline" + ] + case .assetInspector: + [ + "Open an .aimodel package", + "Inspect descriptors and compute types", + "Specialize and verify cache state" + ] + case .runtime: + [ + "Choose an experience", + "Provide its assets and inputs", + "Run and record measured evidence" + ] + case .deviceLab: + [ + "Define the physical target", + "Plan asset delivery", + "Import device-run evidence" + ] + } + } + + var evidenceBoundary: String { + switch self { + case .projects: + "Checksummed storage preserves artifacts and provenance. A stored artifact is not a runtime measurement." + case .appleModels: + "Catalog entries describe pinned Apple recipes. Core AI Lab does not bundle the source model weights." + case .recipes: + "A recipe documents a conversion path. Review its code and upstream license before approving an import." + case .conversion: + "The command, process log, checksums, and validation findings are evidence. A planned command is not a completed conversion." + case .recipeStudio: + "Structural validation checks the authored contract. It does not prove that conversion or runtime execution will succeed." + case .chatterbox: + "Generated audio comes from the bundled local pipeline. Cache reuse does not prove a speed or memory improvement." + case .diarization: + "Speaker labels are anonymous clusters inferred from local media, not verified identities." + case .assetInspector: + "Descriptors and cache state come from Core AI. A preferred compute unit does not prove hardware placement." + case .runtime: + "Only completed runs produce measured timing. Setup choices and comparison identities remain contextual metadata." + case .deviceLab: + "Target preferences and storage plans are proposals. Imported runner output is the physical-device evidence." + } + } } diff --git a/CoreAILab/CoreAIWorkspaceInspectorView.swift b/CoreAILab/CoreAIWorkspaceInspectorView.swift new file mode 100644 index 0000000..aba110c --- /dev/null +++ b/CoreAILab/CoreAIWorkspaceInspectorView.swift @@ -0,0 +1,73 @@ +import SwiftUI + +struct CoreAIWorkspaceInspectorView: View { + let section: CoreAILabSection + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 18) { + inspectorSection( + "Workflow", + systemImage: "point.3.connected.trianglepath.dotted" + ) { + VStack(alignment: .leading, spacing: 12) { + ForEach(section.workflowSteps.indices, id: \.self) { index in + inspectorLabel( + section.workflowSteps[index], + systemImage: "\(index + 1).circle" + ) + } + } + } + + Divider() + .padding(.leading, contentInset) + + inspectorSection("Evidence", systemImage: "checkmark.seal") { + Text(section.evidenceBoundary) + .padding(.leading, contentInset) + .fixedSize(horizontal: false, vertical: true) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + } + .navigationTitle("Workspace") + .inspectorColumnWidth(min: 260, ideal: 300, max: 360) + } + + private let contentInset: CGFloat = 24 + + private func inspectorSection( + _ title: String, + systemImage: String, + @ViewBuilder content: () -> Content + ) -> some View { + VStack(alignment: .leading, spacing: 12) { + inspectorLabel(title, systemImage: systemImage) + .font(.headline) + .foregroundStyle(.secondary) + content() + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func inspectorLabel( + _ title: String, + systemImage: String + ) -> some View { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Image(systemName: systemImage) + .frame(width: 16) + Text(title) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +#Preview { + CoreAIWorkspaceInspectorView(section: .runtime) +} diff --git a/CoreAILab/Features/AppleModels/AppleModelCatalogSourceView.swift b/CoreAILab/Features/AppleModels/AppleModelCatalogSourceView.swift index 189cb68..344c318 100644 --- a/CoreAILab/Features/AppleModels/AppleModelCatalogSourceView.swift +++ b/CoreAILab/Features/AppleModels/AppleModelCatalogSourceView.swift @@ -6,25 +6,30 @@ struct AppleModelCatalogSourceView: View { let sourceRepositoryURL: URL? var body: some View { - VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .firstTextBaseline) { Label("Apple Core AI Models", systemImage: "apple.logo") .font(.headline) - Text("A pinned snapshot of Apple's model registry. Entries are export recipes, not bundled weights.") - .foregroundStyle(.secondary) + Spacer() - HStack(spacing: 16) { - Label("\(modelCount) presets", systemImage: "list.bullet") - Text(sourceRevision.prefix(8)) - .font(.callout.monospaced()) - .foregroundStyle(.secondary) - } + Label("\(modelCount) recipes", systemImage: "list.bullet") + .font(.callout) + .foregroundStyle(.secondary) if let sourceRepositoryURL { - Link("Open apple/coreai-models", destination: sourceRepositoryURL) + Link(destination: sourceRepositoryURL) { + Label { + Text(sourceRevision.prefix(8)) + .monospaced() + } icon: { + Image(systemName: "point.topleft.down.to.point.bottomright.curvepath") + } + } + .font(.callout) } } .padding(.vertical, 8) .accessibilityElement(children: .contain) + .help("Pinned Apple model recipes; model weights are not bundled") } } diff --git a/CoreAILab/Features/AppleModels/AppleModelDetailView.swift b/CoreAILab/Features/AppleModels/AppleModelDetailView.swift index f3525fc..d825ae7 100644 --- a/CoreAILab/Features/AppleModels/AppleModelDetailView.swift +++ b/CoreAILab/Features/AppleModels/AppleModelDetailView.swift @@ -7,7 +7,12 @@ struct AppleModelDetailView: View { var body: some View { Form { Section { - LabeledContent("Model", value: model.huggingFaceID) + LabeledContent("Model") { + Text(model.huggingFaceID) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } LabeledContent( "Platforms", value: model.supportedPlatforms.map(\.rawValue).joined(separator: ", ") @@ -25,13 +30,13 @@ struct AppleModelDetailView: View { Label(model.category.rawValue, systemImage: model.category.systemImage) } - Section("Export with Apple's recipe") { - Text("Clone apple/coreai-models, run this command from its root, then import the resulting .aimodel or resource folder into the Lab.") - .foregroundStyle(.secondary) - + Section { Text(model.labRecommendedExportCommand) .font(.body.monospaced()) .textSelection(.enabled) + .help( + "Run from the root of a local apple/coreai-models checkout, then import the exported asset." + ) if let recipeURL = model.recipeURL(sourceRevision: sourceRevision) { Link("Read the pinned Apple recipe", destination: recipeURL) @@ -41,22 +46,18 @@ struct AppleModelDetailView: View { "Convert This Recipe", value: AppleModelLibraryRoute.conversion(modelID: model.id) ) + } header: { + Label("Export Recipe", systemImage: "terminal") } - Section("Runtime integration") { + Section { Label(model.runtimeSupport.title, systemImage: "shippingbox") - Text(model.runtimeSupport.detail) - .foregroundStyle(.secondary) + .help(model.runtimeSupport.detail) if let productName = model.runtimeSupport.productName { LabeledContent("Swift product", value: productName) } - if model.isRunnableInLab { - Text("The Lab includes this runtime adapter, not converted model weights. Export the model locally under its upstream license, then import the result.") - .foregroundStyle(.secondary) - } - if model.runtimeSupport == .objectDetection, model.isRunnableInLab { NavigationLink( "Open Object Detection Playground", @@ -66,8 +67,11 @@ struct AppleModelDetailView: View { if let segmentationExample = model.segmentationExample { if segmentationExample == .sam3 { - Text("SAM 3 requires accepting Meta's gated Hugging Face license and authenticating with the `hf` command-line tool before export. Credentials stay outside the Lab.") - .foregroundStyle(.secondary) + Label("Upstream license required", systemImage: "exclamationmark.triangle") + .foregroundStyle(.orange) + .help( + "Accept Meta's gated Hugging Face license and authenticate with the hf tool before export. Core AI Lab never reads or stores those credentials." + ) } NavigationLink( segmentationExample.playgroundButtonTitle, @@ -84,8 +88,11 @@ struct AppleModelDetailView: View { if let diffusionExample = model.diffusionExample { if diffusionExample == .stableDiffusion35 { - Text("Stable Diffusion 3.5 weights require accepting Stability AI's gated Hugging Face terms and authenticating with the `hf` command-line tool before export. Credentials stay outside the Lab.") - .foregroundStyle(.secondary) + Label("Upstream license required", systemImage: "exclamationmark.triangle") + .foregroundStyle(.orange) + .help( + "Accept Stability AI's gated Hugging Face terms and authenticate with the hf tool before export. Core AI Lab never reads or stores those credentials." + ) } NavigationLink( diffusionExample.playgroundButtonTitle, @@ -99,16 +106,20 @@ struct AppleModelDetailView: View { value: AppleModelLibraryRoute.audio(audioExample) ) } + } header: { + Label("Runtime Integration", systemImage: "play.rectangle") } - Section("Provenance") { + Section { LabeledContent("Registry revision") { Text(sourceRevision) .font(.callout.monospaced()) .textSelection(.enabled) } - Text("The export recipe and Swift utilities use Apple's BSD-3-Clause repository. Downloaded model weights retain their original authors' licenses and are not redistributed by Core AI Lab.") - .foregroundStyle(.secondary) + LabeledContent("Recipe code", value: "Apple BSD-3-Clause") + LabeledContent("Model weights", value: "Upstream license") + } header: { + Label("Provenance", systemImage: "checkmark.seal") } } .formStyle(.grouped) diff --git a/CoreAILab/Features/AppleModels/AppleModelLibraryView.swift b/CoreAILab/Features/AppleModels/AppleModelLibraryView.swift index 8b0d9f6..46c2e19 100644 --- a/CoreAILab/Features/AppleModels/AppleModelLibraryView.swift +++ b/CoreAILab/Features/AppleModels/AppleModelLibraryView.swift @@ -28,12 +28,17 @@ struct AppleModelLibraryView: View { } ForEach(groups) { group in - Section(group.category.rawValue) { + Section { ForEach(group.models) { entry in NavigationLink(value: entry) { AppleModelRow(model: entry) } } + } header: { + Label( + group.category.rawValue, + systemImage: group.category.systemImage + ) } } } diff --git a/CoreAILab/Features/AppleModels/AppleModelRow.swift b/CoreAILab/Features/AppleModels/AppleModelRow.swift index 29938a8..269e8ce 100644 --- a/CoreAILab/Features/AppleModels/AppleModelRow.swift +++ b/CoreAILab/Features/AppleModels/AppleModelRow.swift @@ -4,30 +4,27 @@ struct AppleModelRow: View { let model: AppleCoreAIModel var body: some View { - VStack(alignment: .leading, spacing: 6) { - HStack(alignment: .firstTextBaseline) { - Text(model.shortName) - .font(.headline) + HStack(alignment: .firstTextBaseline) { + Text(model.shortName) + .font(.headline) - Spacer() + Spacer() - Text(model.supportedPlatforms.map(\.rawValue).joined(separator: " · ")) - .font(.callout) - .foregroundStyle(.secondary) - } - - Text(model.huggingFaceID) - .font(.callout.monospaced()) - .foregroundStyle(.secondary) - - Label(model.runtimeSupport.title, systemImage: runtimeSystemImage) + Label( + model.supportedPlatforms.map(\.rawValue).joined(separator: " · "), + systemImage: platformSystemImage + ) .font(.callout) .foregroundStyle(.secondary) } .padding(.vertical, 4) + .accessibilityElement(children: .combine) + .help("\(model.huggingFaceID) · \(model.runtimeSupport.title)") } - private var runtimeSystemImage: String { - model.isRunnableInLab ? "play.circle.fill" : "shippingbox" + private var platformSystemImage: String { + model.supportedPlatforms.count > 1 + ? "desktopcomputer.and.iphone" + : model.supportedPlatforms.first == .iOS ? "iphone" : "desktopcomputer" } } diff --git a/CoreAILab/Features/AppleModels/Audio/AppleAudioTranscriptionResultView.swift b/CoreAILab/Features/AppleModels/Audio/AppleAudioTranscriptionResultView.swift index f79ec9e..aa1b24b 100644 --- a/CoreAILab/Features/AppleModels/Audio/AppleAudioTranscriptionResultView.swift +++ b/CoreAILab/Features/AppleModels/Audio/AppleAudioTranscriptionResultView.swift @@ -4,7 +4,7 @@ struct AppleAudioTranscriptionResultView: View { let result: AppleAudioTranscriptionResult? var body: some View { - Section("Transcript") { + Section { if let result { if result.transcript.isEmpty { ContentUnavailableView( @@ -28,10 +28,11 @@ struct AppleAudioTranscriptionResultView: View { } else { ContentUnavailableView( "No Transcript Yet", - systemImage: "captions.bubble", - description: Text("Choose a short speech recording and transcribe it locally.") + systemImage: "captions.bubble" ) } + } header: { + Label("Transcript", systemImage: "captions.bubble") } } } diff --git a/CoreAILab/Features/AppleModels/Audio/AppleAudioWorkspaceView.swift b/CoreAILab/Features/AppleModels/Audio/AppleAudioWorkspaceView.swift index 9d99056..8054d66 100644 --- a/CoreAILab/Features/AppleModels/Audio/AppleAudioWorkspaceView.swift +++ b/CoreAILab/Features/AppleModels/Audio/AppleAudioWorkspaceView.swift @@ -32,11 +32,10 @@ struct AppleAudioWorkspaceView: View { LabeledContent("Input", value: "1 × \(info.sampleCount) \(info.scalarTypeName)") LabeledContent("Sample rate", value: "\(Int(info.sampleRate).formatted()) Hz mono") } - Label( - workspace.statusMessage, - systemImage: workspace.isBusy ? "hourglass" : "waveform" - ) - .foregroundStyle(workspace.isBusy ? .primary : .secondary) + if workspace.isBusy { + ProgressView(workspace.statusMessage) + .accessibilityAddTraits(.updatesFrequently) + } } header: { Label(workspace.example.title, systemImage: "waveform.badge.mic") } @@ -46,37 +45,51 @@ struct AppleAudioWorkspaceView: View { context: workspace.runContext ) - Section("Inputs") { - HStack { - Button("Import Wav2Vec2", systemImage: "shippingbox", action: importModel) - Button("Choose Audio", systemImage: "waveform", action: importAudio) - Button("Transcribe", systemImage: "captions.bubble", action: workspace.startTranscription) - .buttonStyle(.borderedProminent) - .disabled(!workspace.canTranscribe) - if workspace.isTranscribing { - Button( - "Cancel", - systemImage: "stop.fill", - role: .destructive, - action: workspace.cancelTranscription - ) - } + Section { + ViewThatFits(in: .horizontal) { + inputActions(axis: .horizontal) + inputActions(axis: .vertical) } - Text("The static Apple recipe accepts at most five seconds. Audio is decoded, downmixed, and resampled to 16 kHz mono before inference.") - .foregroundStyle(.secondary) + } header: { + Label("Model & Audio", systemImage: "waveform.badge.mic") } + .help("Audio is limited to five seconds and prepared as 16 kHz mono before inference.") - Section("Apple Export Command") { + Section { Text(workspace.example.exportCommand) .font(.body.monospaced()) .textSelection(.enabled) + } header: { + Label("Apple Export Command", systemImage: "terminal") } AppleAudioTranscriptionResultView(result: workspace.result) } .formStyle(.grouped) .navigationTitle("Audio Transcription") + .toolbar { +#if os(macOS) + ToolbarItem(placement: .primaryAction) { + if workspace.isTranscribing { + Button( + "Cancel Transcription", + systemImage: "stop.fill", + role: .cancel, + action: workspace.cancelTranscription + ) + } else { + Button( + "Transcribe", + systemImage: "captions.bubble", + action: workspace.startTranscription + ) + .disabled(!workspace.canTranscribe) + .help(workspace.statusMessage) + } + } +#endif + } .fileImporter( isPresented: $isImportingModel, allowedContentTypes: [.coreAIModelAsset, .folder] @@ -89,9 +102,9 @@ struct AppleAudioWorkspaceView: View { ) { result in handleAudioImport(result) } - .alert("Audio Transcription Failed", isPresented: $workspace.isShowingError) { + .alert("Couldn't Transcribe Audio", isPresented: $workspace.isShowingError) { } message: { - Text(workspace.errorMessage ?? "The request could not be completed.") + Text(workspace.errorMessage ?? "Check the model and audio files, then try again.") } .task(id: initialModelURL) { if let initialModelURL { @@ -116,7 +129,7 @@ struct AppleAudioWorkspaceView: View { await workspace.loadModel(from: url) } case .failure(let error): - workspace.presentImportError(error) + presentSelectionError(error) } } @@ -125,6 +138,36 @@ struct AppleAudioWorkspaceView: View { case .success(let url): workspace.selectAudio(url) case .failure(let error): + presentSelectionError(error) + } + } + + private func inputActions(axis: Axis) -> some View { + let layout = axis == .horizontal + ? AnyLayout(HStackLayout()) + : AnyLayout(VStackLayout(alignment: .leading)) + + return layout { + Button("Import Wav2Vec2 Model", systemImage: "shippingbox", action: importModel) + Button("Choose Audio", systemImage: "waveform", action: importAudio) +#if !os(macOS) + Button("Transcribe", systemImage: "captions.bubble", action: workspace.startTranscription) + .buttonStyle(.borderedProminent) + .disabled(!workspace.canTranscribe) + if workspace.isTranscribing { + Button( + "Cancel", + systemImage: "stop.fill", + role: .cancel, + action: workspace.cancelTranscription + ) + } +#endif + } + } + + private func presentSelectionError(_ error: any Error) { + if (error as? CocoaError)?.code != .userCancelled { workspace.presentImportError(error) } } diff --git a/CoreAILab/Features/AppleModels/Diffusion/AppleDiffusionResultView.swift b/CoreAILab/Features/AppleModels/Diffusion/AppleDiffusionResultView.swift index 3fd18a0..5334d92 100644 --- a/CoreAILab/Features/AppleModels/Diffusion/AppleDiffusionResultView.swift +++ b/CoreAILab/Features/AppleModels/Diffusion/AppleDiffusionResultView.swift @@ -4,7 +4,7 @@ struct AppleDiffusionResultView: View { let result: AppleDiffusionResult? var body: some View { - Section("Generated Image") { + Section { if let result { Image(decorative: result.image, scale: 1) .resizable() @@ -18,10 +18,11 @@ struct AppleDiffusionResultView: View { } else { ContentUnavailableView( "No Image Yet", - systemImage: "photo.badge.plus", - description: Text("Import a diffusion bundle, enter a prompt, and generate locally.") + systemImage: "photo.badge.plus" ) } + } header: { + Label("Generated Image", systemImage: "photo") } } } diff --git a/CoreAILab/Features/AppleModels/Diffusion/AppleDiffusionWorkspaceView.swift b/CoreAILab/Features/AppleModels/Diffusion/AppleDiffusionWorkspaceView.swift index 75386c5..305a2dc 100644 --- a/CoreAILab/Features/AppleModels/Diffusion/AppleDiffusionWorkspaceView.swift +++ b/CoreAILab/Features/AppleModels/Diffusion/AppleDiffusionWorkspaceView.swift @@ -30,11 +30,10 @@ struct AppleDiffusionWorkspaceView: View { LabeledContent("Pipeline", value: info.pipelineName) LabeledContent("Output", value: "\(info.width) × \(info.height)") } - Label( - workspace.statusMessage, - systemImage: workspace.isBusy ? "hourglass" : "wand.and.sparkles" - ) - .foregroundStyle(workspace.isBusy ? .primary : .secondary) + if workspace.isBusy { + ProgressView(workspace.statusMessage) + .accessibilityAddTraits(.updatesFrequently) + } } header: { Label(workspace.example.title, systemImage: "wand.and.sparkles") } @@ -44,23 +43,23 @@ struct AppleDiffusionWorkspaceView: View { context: workspace.runContext ) - Section("Pipeline Bundle") { + Section { Button( "Import Diffusion Bundle", systemImage: "shippingbox", action: importPipeline ) - Text("Import the entire folder produced by `coreai.diffusion.export`. The Lab reads its metadata and selects Apple's Stable Diffusion, SD3, or FLUX.2 runtime automatically.") - .foregroundStyle(.secondary) + .help("Choose a folder produced by coreai.diffusion.export.") + } header: { + Label("Pipeline Bundle", systemImage: "shippingbox") } - Section("Prompt") { + Section { TextField("Describe an image", text: $workspace.prompt, axis: .vertical) .lineLimit(3...8) .disabled(!workspace.canEditGenerationInputs) if workspace.modelInfo?.supportsNegativePrompt == false { - Text("FLUX.2 does not consume a negative prompt.") - .foregroundStyle(.secondary) + LabeledContent("Negative prompt", value: "Not supported") } else { TextField("Negative prompt", text: $workspace.negativePrompt, axis: .vertical) .lineLimit(2...5) @@ -77,34 +76,47 @@ struct AppleDiffusionWorkspaceView: View { .disabled(!workspace.canEditGenerationInputs) } - HStack { - Button("Generate", systemImage: "play.fill", action: workspace.startGeneration) - .buttonStyle(.borderedProminent) - .disabled(!workspace.canGenerate) - if workspace.isGenerating { - Button( - "Cancel", - systemImage: "stop.fill", - role: .destructive, - action: workspace.cancelGeneration - ) - } +#if !os(macOS) + ViewThatFits(in: .horizontal) { + generationActions(axis: .horizontal) + generationActions(axis: .vertical) } +#endif + } header: { + Label("Prompt", systemImage: "text.bubble") } AppleDiffusionResultView(result: workspace.result) } .formStyle(.grouped) .navigationTitle("Diffusion Playground") + .toolbar { +#if os(macOS) + ToolbarItem(placement: .primaryAction) { + if workspace.isGenerating { + Button( + "Cancel Generation", + systemImage: "stop.fill", + role: .cancel, + action: workspace.cancelGeneration + ) + } else { + Button("Generate", systemImage: "play.fill", action: workspace.startGeneration) + .disabled(!workspace.canGenerate) + .help(workspace.statusMessage) + } + } +#endif + } .fileImporter( isPresented: $isImportingPipeline, allowedContentTypes: [.folder] ) { result in handlePipelineImport(result) } - .alert("Diffusion Failed", isPresented: $workspace.isShowingError) { + .alert("Couldn't Generate the Image", isPresented: $workspace.isShowingError) { } message: { - Text(workspace.errorMessage ?? "The request could not be completed.") + Text(workspace.errorMessage ?? "Check the pipeline bundle and prompt, then try again.") } .task(id: initialModelURL) { if let initialModelURL { @@ -125,7 +137,31 @@ struct AppleDiffusionWorkspaceView: View { await workspace.loadPipeline(from: url) } case .failure(let error): - workspace.presentImportError(error) + if (error as? CocoaError)?.code != .userCancelled { + workspace.presentImportError(error) + } + } + } + +#if !os(macOS) + private func generationActions(axis: Axis) -> some View { + let layout = axis == .horizontal + ? AnyLayout(HStackLayout()) + : AnyLayout(VStackLayout(alignment: .leading)) + + return layout { + Button("Generate", systemImage: "play.fill", action: workspace.startGeneration) + .buttonStyle(.borderedProminent) + .disabled(!workspace.canGenerate) + if workspace.isGenerating { + Button( + "Cancel", + systemImage: "stop.fill", + role: .cancel, + action: workspace.cancelGeneration + ) + } } } +#endif } diff --git a/CoreAILab/Features/AppleModels/Language/AppleLanguageResponseView.swift b/CoreAILab/Features/AppleModels/Language/AppleLanguageResponseView.swift index a76611b..28d8d07 100644 --- a/CoreAILab/Features/AppleModels/Language/AppleLanguageResponseView.swift +++ b/CoreAILab/Features/AppleModels/Language/AppleLanguageResponseView.swift @@ -4,17 +4,18 @@ struct AppleLanguageResponseView: View { let response: String var body: some View { - Section("Response") { + Section { if response.isEmpty { ContentUnavailableView( "No Response Yet", - systemImage: "text.bubble", - description: Text("Import Qwen, enter a prompt, and generate locally.") + systemImage: "text.bubble" ) } else { Text(response) .textSelection(.enabled) } + } header: { + Label("Response", systemImage: "text.bubble") } } } diff --git a/CoreAILab/Features/AppleModels/Language/AppleLanguageWorkspaceView.swift b/CoreAILab/Features/AppleModels/Language/AppleLanguageWorkspaceView.swift index 53ecd49..157b149 100644 --- a/CoreAILab/Features/AppleModels/Language/AppleLanguageWorkspaceView.swift +++ b/CoreAILab/Features/AppleModels/Language/AppleLanguageWorkspaceView.swift @@ -26,11 +26,10 @@ struct AppleLanguageWorkspaceView: View { Form { Section { LabeledContent("Model", value: workspace.modelName ?? "Not loaded") - Label( - workspace.statusMessage, - systemImage: workspace.isBusy ? "hourglass" : "text.bubble" - ) - .foregroundStyle(workspace.isBusy ? .primary : .secondary) + if workspace.isBusy { + ProgressView(workspace.statusMessage) + .accessibilityAddTraits(.updatesFrequently) + } } header: { Label(workspace.example.title, systemImage: "text.bubble.fill") } @@ -40,11 +39,10 @@ struct AppleLanguageWorkspaceView: View { context: workspace.runContext ) - Section("Model Bundle") { - HStack { - Button("Import Qwen Bundle", systemImage: "shippingbox", action: importModel) - Button("New Session", systemImage: "arrow.counterclockwise", action: resetSession) - .disabled(workspace.modelName == nil || workspace.isBusy) + Section { + ViewThatFits(in: .horizontal) { + modelActions(axis: .horizontal) + modelActions(axis: .vertical) } LabeledContent("macOS export") { @@ -57,9 +55,11 @@ struct AppleLanguageWorkspaceView: View { .font(.body.monospaced()) .textSelection(.enabled) } + } header: { + Label("Model Bundle", systemImage: "shippingbox") } - Section("Prompt") { + Section { TextField("Ask Qwen", text: $workspace.prompt, axis: .vertical) .lineLimit(3...8) .disabled(!workspace.canEditGenerationInputs) @@ -71,29 +71,47 @@ struct AppleLanguageWorkspaceView: View { ) .disabled(!workspace.canEditGenerationInputs) - HStack { - Button("Generate", systemImage: "play.fill", action: workspace.startGeneration) - .buttonStyle(.borderedProminent) - .disabled(!workspace.canGenerate) - if workspace.isGenerating { - Button("Cancel", systemImage: "stop.fill", role: .destructive, action: workspace.cancelGeneration) - } +#if !os(macOS) + ViewThatFits(in: .horizontal) { + generationActions(axis: .horizontal) + generationActions(axis: .vertical) } +#endif + } header: { + Label("Prompt", systemImage: "text.bubble") } AppleLanguageResponseView(response: workspace.response) } .formStyle(.grouped) .navigationTitle("\(workspace.example.title) Language Model") + .toolbar { +#if os(macOS) + ToolbarItem(placement: .primaryAction) { + if workspace.isGenerating { + Button( + "Cancel Generation", + systemImage: "stop.fill", + role: .cancel, + action: workspace.cancelGeneration + ) + } else { + Button("Generate", systemImage: "play.fill", action: workspace.startGeneration) + .disabled(!workspace.canGenerate) + .help(workspace.statusMessage) + } + } +#endif + } .fileImporter( isPresented: $isImportingModel, allowedContentTypes: [.folder] ) { result in handleModelImport(result) } - .alert("Language Model Failed", isPresented: $workspace.isShowingError) { + .alert("Couldn't Generate a Response", isPresented: $workspace.isShowingError) { } message: { - Text(workspace.errorMessage ?? "The request could not be completed.") + Text(workspace.errorMessage ?? "Check the model bundle and prompt, then try again.") } .task(id: initialModelURL) { if let initialModelURL { @@ -120,7 +138,47 @@ struct AppleLanguageWorkspaceView: View { await workspace.loadModel(from: url) } case .failure(let error): - workspace.presentImportError(error) + if (error as? CocoaError)?.code != .userCancelled { + workspace.presentImportError(error) + } + } + } + + private func modelActions(axis: Axis) -> some View { + adaptiveLayout(axis: axis) { + Button("Import Qwen Bundle", systemImage: "shippingbox", action: importModel) + Button("New Session", systemImage: "arrow.counterclockwise", action: resetSession) + .disabled(workspace.modelName == nil || workspace.isBusy) + } + } + +#if !os(macOS) + private func generationActions(axis: Axis) -> some View { + adaptiveLayout(axis: axis) { + Button("Generate", systemImage: "play.fill", action: workspace.startGeneration) + .buttonStyle(.borderedProminent) + .disabled(!workspace.canGenerate) + if workspace.isGenerating { + Button( + "Cancel", + systemImage: "stop.fill", + role: .cancel, + action: workspace.cancelGeneration + ) + } + } + } +#endif + + private func adaptiveLayout( + axis: Axis, + @ViewBuilder content: () -> Content + ) -> some View { + let layout = axis == .horizontal + ? AnyLayout(HStackLayout()) + : AnyLayout(VStackLayout(alignment: .leading)) + return layout { + content() } } } diff --git a/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionHeaderView.swift b/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionHeaderView.swift index b96b093..a76eb73 100644 --- a/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionHeaderView.swift +++ b/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionHeaderView.swift @@ -11,21 +11,15 @@ struct AppleObjectDetectionHeaderView: View { Label("YOLOS Tiny", systemImage: "viewfinder") .font(.title2.bold()) - Text("The first runnable Apple gallery model uses Apple's export recipe and CoreAIObjectDetection Swift package.") - .foregroundStyle(.secondary) - LabeledContent("Model", value: modelName ?? "Not imported") LabeledContent("Image", value: imageName ?? "Not selected") - HStack(spacing: 8) { - if isBusy { - ProgressView() - .controlSize(.small) - } - Text(statusMessage) - .foregroundStyle(.secondary) + if isBusy { + ProgressView(statusMessage) + .accessibilityAddTraits(.updatesFrequently) } } + .help("Uses Apple's YOLOS export recipe and CoreAIObjectDetection runtime.") .accessibilityElement(children: .contain) } } diff --git a/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionPreviewView.swift b/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionPreviewView.swift index ab134be..b0ba7d4 100644 --- a/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionPreviewView.swift +++ b/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionPreviewView.swift @@ -17,7 +17,12 @@ struct AppleObjectDetectionPreviewView: View { } .accessibilityLabel("Object detection source image") - if !detections.isEmpty { + if detections.isEmpty { + ContentUnavailableView( + "No Detections Yet", + systemImage: "viewfinder" + ) + } else { Table(detections) { TableColumn("Object", value: \.label) TableColumn("Confidence") { detection in diff --git a/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionWorkspaceView.swift b/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionWorkspaceView.swift index 48952e9..647ed3f 100644 --- a/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionWorkspaceView.swift +++ b/CoreAILab/Features/AppleModels/ObjectDetection/AppleObjectDetectionWorkspaceView.swift @@ -19,35 +19,40 @@ struct AppleObjectDetectionWorkspaceView: View { } var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 24) { + Form { + Section { AppleObjectDetectionHeaderView( modelName: workspace.modelName, imageName: workspace.imageName, statusMessage: workspace.statusMessage, isBusy: workspace.isLoadingModel || workspace.isRunning ) + } - HStack(spacing: 12) { - Button("Import YOLOS Model", systemImage: "shippingbox", action: importModel) - Button("Choose Image", systemImage: "photo", action: importImage) - Button("Run Detection", systemImage: "play.fill", action: runDetection) - .buttonStyle(.borderedProminent) - .disabled(!workspace.canRun) + Section { + ViewThatFits(in: .horizontal) { + inputActions(axis: .horizontal) + inputActions(axis: .vertical) } .disabled(workspace.isBusy) + } header: { + Label("Model & Image", systemImage: "viewfinder") + } - CoreAIRuntimeLifecycleView( - coordinator: workspace.runCoordinator, - context: workspace.runContext - ) + CoreAIRuntimeLifecycleView( + coordinator: workspace.runCoordinator, + context: workspace.runContext + ) - Text("Export command") - .font(.headline) + Section { Text("uv run models/yolo/export.py --model hustvl/yolos-tiny --dtype float16") .font(.body.monospaced()) .textSelection(.enabled) + } header: { + Label("Apple Export Command", systemImage: "terminal") + } + Section { if let sourceImage = workspace.sourceImage { AppleObjectDetectionPreviewView( image: sourceImage, @@ -56,15 +61,24 @@ struct AppleObjectDetectionWorkspaceView: View { } else { ContentUnavailableView( "Choose an Image", - systemImage: "photo", - description: Text("The result will show Apple's COCO labels, confidence, and bounding boxes.") + systemImage: "photo" ) } + } header: { + Label("Result", systemImage: "viewfinder") } - .frame(maxWidth: 1_200, alignment: .leading) - .padding(32) } + .formStyle(.grouped) .navigationTitle("Object Detection") + .toolbar { +#if os(macOS) + ToolbarItem(placement: .primaryAction) { + Button("Run Detection", systemImage: "play.fill", action: runDetection) + .disabled(!workspace.canRun) + .help(workspace.statusMessage) + } +#endif + } .fileImporter( isPresented: $isImportingModel, allowedContentTypes: [.coreAIModelAsset, .folder] @@ -77,9 +91,9 @@ struct AppleObjectDetectionWorkspaceView: View { ) { result in handleImageImport(result) } - .alert("Object Detection Failed", isPresented: $workspace.isShowingError) { + .alert("Couldn't Detect Objects", isPresented: $workspace.isShowingError) { } message: { - Text(workspace.errorMessage ?? "The request could not be completed.") + Text(workspace.errorMessage ?? "Check the model and image, then try again.") } } @@ -104,7 +118,7 @@ struct AppleObjectDetectionWorkspaceView: View { await workspace.loadModel(from: url) } case .failure(let error): - workspace.presentImportError(error) + presentSelectionError(error) } } @@ -113,6 +127,28 @@ struct AppleObjectDetectionWorkspaceView: View { case .success(let url): workspace.loadImage(from: url) case .failure(let error): + presentSelectionError(error) + } + } + + private func inputActions(axis: Axis) -> some View { + let layout = axis == .horizontal + ? AnyLayout(HStackLayout(spacing: 12)) + : AnyLayout(VStackLayout(alignment: .leading)) + + return layout { + Button("Import YOLOS Model", systemImage: "shippingbox", action: importModel) + Button("Choose Image", systemImage: "photo", action: importImage) +#if !os(macOS) + Button("Run Detection", systemImage: "play.fill", action: runDetection) + .buttonStyle(.borderedProminent) + .disabled(!workspace.canRun) +#endif + } + } + + private func presentSelectionError(_ error: any Error) { + if (error as? CocoaError)?.code != .userCancelled { workspace.presentImportError(error) } } diff --git a/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationPreviewView.swift b/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationPreviewView.swift index 7bf13fc..c38b305 100644 --- a/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationPreviewView.swift +++ b/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationPreviewView.swift @@ -5,7 +5,7 @@ struct AppleSegmentationPreviewView: View { let result: AppleSegmentationResult? var body: some View { - Section("Result") { + Section { if let image { Image( image, @@ -26,10 +26,11 @@ struct AppleSegmentationPreviewView: View { } else { ContentUnavailableView( "No Image", - systemImage: "photo", - description: Text("Choose an image to preview segmentation results.") + systemImage: "photo" ) } + } header: { + Label("Result", systemImage: "square.stack.3d.up") } } } diff --git a/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationQueryControlsView.swift b/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationQueryControlsView.swift index 532de40..00832b3 100644 --- a/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationQueryControlsView.swift +++ b/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationQueryControlsView.swift @@ -4,7 +4,7 @@ struct AppleSegmentationQueryControlsView: View { @Bindable var workspace: AppleSegmentationWorkspaceModel var body: some View { - Section(workspace.example.usesTextPrompt ? "Text Prompt" : "Point Prompt") { + Section { if workspace.example.usesTextPrompt { TextField( "Object to segment", @@ -12,8 +12,7 @@ struct AppleSegmentationQueryControlsView: View { axis: .vertical ) .lineLimit(2...4) - Text("SAM 3 uses the tokenizer bundled with Apple's exported resource folder.") - .foregroundStyle(.secondary) + .help("SAM 3 uses the tokenizer bundled with the exported resource folder.") } else if workspace.sourceImage != nil { LabeledContent("Horizontal position") { Text(workspace.pointX, format: .number.precision(.fractionLength(0))) @@ -27,15 +26,18 @@ struct AppleSegmentationQueryControlsView: View { } Slider(value: $workspace.pointY, in: 0...workspace.imageHeight) - Text("Coordinates are measured in pixels from the image's top-left corner.") - .foregroundStyle(.secondary) + .help("Coordinates use pixels from the image's top-left corner.") } else { ContentUnavailableView( "Choose an Image", - systemImage: "point.bottomleft.forward.to.point.topright.scurvepath", - description: Text("Point controls appear after an image is loaded.") + systemImage: "point.bottomleft.forward.to.point.topright.scurvepath" ) } + } header: { + Label( + workspace.example.usesTextPrompt ? "Text Prompt" : "Point Prompt", + systemImage: workspace.example.usesTextPrompt ? "text.cursor" : "scope" + ) } .disabled(workspace.isBusy) } diff --git a/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationWorkspaceView.swift b/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationWorkspaceView.swift index 8657fb8..cce38d3 100644 --- a/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationWorkspaceView.swift +++ b/CoreAILab/Features/AppleModels/Segmentation/AppleSegmentationWorkspaceView.swift @@ -28,11 +28,10 @@ struct AppleSegmentationWorkspaceView: View { Section { LabeledContent("Model", value: workspace.modelName ?? "Not loaded") LabeledContent("Image", value: workspace.imageName ?? "Not loaded") - Label( - workspace.statusMessage, - systemImage: statusSystemImage - ) - .foregroundStyle(workspace.isBusy ? .primary : .secondary) + if workspace.isBusy { + ProgressView(workspace.statusMessage) + .accessibilityAddTraits(.updatesFrequently) + } } header: { Label(workspace.example.title, systemImage: "square.stack.3d.up") } @@ -42,19 +41,22 @@ struct AppleSegmentationWorkspaceView: View { context: workspace.runContext ) - Section("Run Apple's Export") { - HStack { - Button("Import Model Bundle", systemImage: "shippingbox", action: importModel) - Button("Choose Image", systemImage: "photo", action: importImage) - Button("Run Segmentation", systemImage: "play.fill", action: runSegmentation) - .buttonStyle(.borderedProminent) - .disabled(!workspace.canRun) + Section { + ViewThatFits(in: .horizontal) { + inputActions(axis: .horizontal) + inputActions(axis: .vertical) } .disabled(workspace.isBusy) + } header: { + Label("Model & Image", systemImage: "square.stack.3d.up") + } + Section { Text(workspace.example.exportCommand) .font(.body.monospaced()) .textSelection(.enabled) + } header: { + Label("Apple Export Command", systemImage: "terminal") } AppleSegmentationQueryControlsView(workspace: workspace) @@ -65,6 +67,15 @@ struct AppleSegmentationWorkspaceView: View { } .formStyle(.grouped) .navigationTitle("\(workspace.example.title) Segmentation") + .toolbar { +#if os(macOS) + ToolbarItem(placement: .primaryAction) { + Button("Run Segmentation", systemImage: "play.fill", action: runSegmentation) + .disabled(!workspace.canRun) + .help(workspace.statusMessage) + } +#endif + } .fileImporter( isPresented: $isImportingModel, allowedContentTypes: [.folder] @@ -77,9 +88,9 @@ struct AppleSegmentationWorkspaceView: View { ) { result in handleImageImport(result) } - .alert("Segmentation Failed", isPresented: $workspace.isShowingError) { + .alert("Couldn't Segment the Image", isPresented: $workspace.isShowingError) { } message: { - Text(workspace.errorMessage ?? "The request could not be completed.") + Text(workspace.errorMessage ?? "Check the model bundle and image, then try again.") } .task(id: initialModelURL) { if let initialModelURL { @@ -92,19 +103,6 @@ struct AppleSegmentationWorkspaceView: View { isImportingModel = true } - private var statusSystemImage: String { - if workspace.isBusy { - return "hourglass" - } - if workspace.isShowingError { - return "exclamationmark.triangle" - } - if workspace.modelName != nil, workspace.sourceImage != nil { - return "checkmark.circle" - } - return "info.circle" - } - private func importImage() { isImportingImage = true } @@ -122,7 +120,7 @@ struct AppleSegmentationWorkspaceView: View { await workspace.loadModel(from: url) } case .failure(let error): - workspace.presentImportError(error) + presentSelectionError(error) } } @@ -131,6 +129,28 @@ struct AppleSegmentationWorkspaceView: View { case .success(let url): workspace.loadImage(from: url) case .failure(let error): + presentSelectionError(error) + } + } + + private func inputActions(axis: Axis) -> some View { + let layout = axis == .horizontal + ? AnyLayout(HStackLayout()) + : AnyLayout(VStackLayout(alignment: .leading)) + + return layout { + Button("Import Model Bundle", systemImage: "shippingbox", action: importModel) + Button("Choose Image", systemImage: "photo", action: importImage) +#if !os(macOS) + Button("Run Segmentation", systemImage: "play.fill", action: runSegmentation) + .buttonStyle(.borderedProminent) + .disabled(!workspace.canRun) +#endif + } + } + + private func presentSelectionError(_ error: any Error) { + if (error as? CocoaError)?.code != .userCancelled { workspace.presentImportError(error) } } diff --git a/CoreAILab/Features/AssetInspector/CoreAIAssetInspectorView.swift b/CoreAILab/Features/AssetInspector/CoreAIAssetInspectorView.swift index 9eb218f..ad3a246 100644 --- a/CoreAILab/Features/AssetInspector/CoreAIAssetInspectorView.swift +++ b/CoreAILab/Features/AssetInspector/CoreAIAssetInspectorView.swift @@ -30,16 +30,12 @@ struct CoreAIAssetInspectorView: View { } else if workspace.isInspecting { ContentUnavailableView { Label("Inspecting Model", systemImage: "doc.text.magnifyingglass") - } description: { - Text("Reading metadata, functions, and compute types from the Core AI asset.") } actions: { ProgressView() } } else { ContentUnavailableView { Label("Inspect a Core AI Model", systemImage: "doc.text.magnifyingglass") - } description: { - Text("Open any exported .aimodel package, including assets produced by Apple's coreai-models recipes.") } actions: { Button("Open Model", systemImage: "folder", action: openModelPicker) .buttonStyle(.borderedProminent) @@ -48,9 +44,12 @@ struct CoreAIAssetInspectorView: View { } .navigationTitle("Asset Inspector") .toolbar { - ToolbarItem(placement: .primaryAction) { - Button("Open Model", systemImage: "folder", action: openModelPicker) - .disabled(workspace.phase.isBusy) + if workspace.report != nil { + ToolbarItem(placement: .primaryAction) { + Button("Open Model", systemImage: "folder", action: openModelPicker) + .disabled(workspace.phase.isBusy) + .keyboardShortcut("o", modifiers: .command) + } } } .fileImporter( @@ -59,9 +58,9 @@ struct CoreAIAssetInspectorView: View { ) { result in handleModelImport(result) } - .alert("Core AI Operation Failed", isPresented: $workspace.isShowingError) { + .alert("Couldn't Inspect the Model", isPresented: $workspace.isShowingError) { } message: { - Text(workspace.errorMessage ?? "The Core AI operation failed.") + Text(workspace.errorMessage ?? "Check the model asset and try again.") } .task(id: initialURL) { do { @@ -93,7 +92,9 @@ struct CoreAIAssetInspectorView: View { await workspace.inspect(url: url) } case .failure(let error): - workspace.presentImportError(error) + if (error as? CocoaError)?.code != .userCancelled { + workspace.presentImportError(error) + } } } diff --git a/CoreAILab/Features/AssetInspector/CoreAIAssetReportView.swift b/CoreAILab/Features/AssetInspector/CoreAIAssetReportView.swift index 092345f..e80e80c 100644 --- a/CoreAILab/Features/AssetInspector/CoreAIAssetReportView.swift +++ b/CoreAILab/Features/AssetInspector/CoreAIAssetReportView.swift @@ -6,22 +6,29 @@ struct CoreAIAssetReportView: View { let allowsCacheRemoval: Bool var body: some View { - List { - Section("Asset") { + Form { + Section { LabeledContent("Name", value: report.url.lastPathComponent) - LabeledContent("Valid Core AI asset", value: report.isValid ? "Yes" : "No") + LabeledContent("Core AI asset") { + Label( + report.isValid ? "Valid" : "Invalid", + systemImage: report.isValid + ? "checkmark.circle.fill" + : "xmark.circle.fill" + ) + } LabeledContent("Author", value: valueOrFallback(report.author)) LabeledContent("License", value: valueOrFallback(report.license)) if !report.description.isEmpty { - Text(report.description) - .foregroundStyle(.secondary) + LabeledContent("Description", value: report.description) } + } header: { + Label("Asset", systemImage: "shippingbox") } - Section("Functions") { + Section { if report.functions.isEmpty { - Text("No functions were declared in the asset summary.") - .foregroundStyle(.secondary) + Label("No Functions Declared", systemImage: "minus.circle") } else { ForEach(report.functions) { function in DisclosureGroup { @@ -43,23 +50,25 @@ struct CoreAIAssetReportView: View { } } } + } header: { + Label("Functions", systemImage: "function") } - Section("Compute Types") { + Section { if report.computeTypes.isEmpty { - Text("No compute types were reported.") - .foregroundStyle(.secondary) + Label("Not Reported", systemImage: "minus.circle") } else { ForEach(report.computeTypes, id: \.self) { computeType in Text(computeType) } } + } header: { + Label("Compute Types", systemImage: "cpu") } - Section("Storage Types") { + Section { if report.storageTypes.isEmpty { - Text("No storage statistics were reported.") - .foregroundStyle(.secondary) + Label("Not Reported", systemImage: "minus.circle") } else { ForEach(report.storageTypes) { storageType in LabeledContent( @@ -69,12 +78,13 @@ struct CoreAIAssetReportView: View { ) } } + } header: { + Label("Storage Types", systemImage: "internaldrive") } - Section("Operation Distribution") { + Section { if report.operationDistribution.isEmpty { - Text("No operation statistics were reported.") - .foregroundStyle(.secondary) + Label("Not Reported", systemImage: "minus.circle") } else { ForEach(report.operationDistribution) { operation in LabeledContent( @@ -84,12 +94,16 @@ struct CoreAIAssetReportView: View { ) } } + } header: { + Label("Operation Distribution", systemImage: "chart.bar.xaxis") } - Section("Source") { + Section { Text(report.url.path) .font(.callout.monospaced()) .textSelection(.enabled) + } header: { + Label("Source", systemImage: "folder") } CoreAISpecializationControlsView( @@ -97,6 +111,7 @@ struct CoreAIAssetReportView: View { allowsCacheRemoval: allowsCacheRemoval ) } + .formStyle(.grouped) } private func valueOrFallback(_ value: String) -> String { diff --git a/CoreAILab/Features/AssetInspector/CoreAISpecializationControlsView.swift b/CoreAILab/Features/AssetInspector/CoreAISpecializationControlsView.swift index f2e302f..aac655e 100644 --- a/CoreAILab/Features/AssetInspector/CoreAISpecializationControlsView.swift +++ b/CoreAILab/Features/AssetInspector/CoreAISpecializationControlsView.swift @@ -6,7 +6,7 @@ struct CoreAISpecializationControlsView: View { var allowsCacheRemoval = true var body: some View { - Section("Specialization & Cache") { + Section { Picker("Compute profile", selection: $workspace.selectedProfile) { ForEach(CoreAISpecializationProfile.allCases) { profile in Text(profile.title) @@ -18,10 +18,7 @@ struct CoreAISpecializationControlsView: View { .onChange(of: workspace.selectedProfile) { refreshCacheStatus() } - - Text(workspace.selectedProfile.detail) - .font(.subheadline) - .foregroundStyle(.secondary) + .help(workspace.selectedProfile.detail) Toggle( "Expect frequent input reshapes", @@ -31,12 +28,9 @@ struct CoreAISpecializationControlsView: View { .onChange(of: workspace.expectFrequentReshapes) { refreshCacheStatus() } - - Text( - "This Core AI specialization option is part of the cache identity. Measure both settings for dynamic-shape workloads instead of assuming one is faster." + .help( + "This setting is part of the cache identity. Measure both configurations for dynamic-shape workloads." ) - .font(.subheadline) - .foregroundStyle(.secondary) LabeledContent("Selected configuration") { Label( @@ -78,6 +72,11 @@ struct CoreAISpecializationControlsView: View { action: prepareAssetRemoval ) } + .help( + allowsCacheRemoval + ? "Core AI exposes hit, miss, and deletion for this known asset." + : "Remove project-owned cache configurations from the artifact detail screen." + ) .confirmationDialog( workspace.cacheRemovalTitle, isPresented: $workspace.isConfirmingCacheRemoval, @@ -96,21 +95,26 @@ struct CoreAISpecializationControlsView: View { .disabled(workspace.phase.isBusy || isInteractionDisabled) if workspace.phase.isBusy { - Label("Core AI operation in progress", systemImage: "hourglass") - .foregroundStyle(.secondary) + ProgressView(operationTitle) + .accessibilityAddTraits(.updatesFrequently) } + } header: { + Label("Specialization & Cache", systemImage: "cpu") + } + } - Text("Core AI exposes hit/miss and deletion for known assets, but not cache paths, entry sizes, or a complete inventory.") - .font(.subheadline) - .foregroundStyle(.secondary) - - if !allowsCacheRemoval { - Text( - "Remove project-owned cache configurations from the artifact detail screen so configurations referenced by another project remain available." - ) - .font(.subheadline) - .foregroundStyle(.secondary) - } + private var operationTitle: String { + switch workspace.phase { + case .inspecting: + "Inspecting model…" + case .checkingCache: + "Checking specialization cache…" + case .specializing: + "Specializing model…" + case .removingCache: + "Deleting cached specialization…" + case .idle, .ready: + "Updating Core AI state…" } } diff --git a/CoreAILab/Features/Chatterbox/ChatterboxGenerationSection.swift b/CoreAILab/Features/Chatterbox/ChatterboxGenerationSection.swift index c3e0262..979ef06 100644 --- a/CoreAILab/Features/Chatterbox/ChatterboxGenerationSection.swift +++ b/CoreAILab/Features/Chatterbox/ChatterboxGenerationSection.swift @@ -3,6 +3,7 @@ import SwiftUI struct ChatterboxGenerationSection: View { let canGenerate: Bool let isWorking: Bool + let workingActionTitle: String let statusMessage: String let result: ChatterboxGenerationResult? let isPlaying: Bool @@ -11,34 +12,43 @@ struct ChatterboxGenerationSection: View { var body: some View { Section { - Button( - "Generate speech", - systemImage: "play.circle.fill", - action: generateAction - ) +#if !os(macOS) + Button(action: generateAction) { + Label { + Text(isWorking ? workingActionTitle : "Generate Speech") + } icon: { + if isWorking { + ProgressView() + .controlSize(.small) + } else { + Image(systemName: "play.circle.fill") + } + } + } .buttonStyle(.borderedProminent) .disabled(!canGenerate) +#endif if isWorking { - ProgressView(statusMessage) + Text(statusMessage) + .foregroundStyle(.secondary) + .accessibilityAddTraits(.updatesFrequently) } if let result { Button( - isPlaying ? "Stop playback" : "Play generated speech", + isPlaying ? "Stop Playback" : "Play Generated Speech", systemImage: isPlaying ? "stop.fill" : "speaker.wave.3.fill", action: playbackAction ) LabeledContent( - "Generation", - value: result.elapsedTime, - format: .number.precision(.fractionLength(2)) + "Generation time", + value: "\(result.elapsedTime.formatted(.number.precision(.fractionLength(2)))) seconds" ) LabeledContent( - "Audio", - value: result.audioDuration, - format: .number.precision(.fractionLength(2)) + "Audio duration", + value: "\(result.audioDuration.formatted(.number.precision(.fractionLength(2)))) seconds" ) LabeledContent( "Real-time factor", @@ -55,11 +65,12 @@ struct ChatterboxGenerationSection: View { item: result.audioURL, preview: SharePreview("Chatterbox Core AI audio") ) { - Label("Share generated audio", systemImage: "square.and.arrow.up") + Label("Share Generated Audio", systemImage: "square.and.arrow.up") } } - } footer: { - Text("The first launch specializes the bundled graphs. Later runs reuse Core AI's persistent cache.") + } header: { + Label("Generate & Playback", systemImage: "speaker.wave.3") } + .help("The first launch specializes the bundled graphs; later runs may reuse Core AI's cache.") } } diff --git a/CoreAILab/Features/Chatterbox/ChatterboxHeroView.swift b/CoreAILab/Features/Chatterbox/ChatterboxHeroView.swift index 66b1f88..8383033 100644 --- a/CoreAILab/Features/Chatterbox/ChatterboxHeroView.swift +++ b/CoreAILab/Features/Chatterbox/ChatterboxHeroView.swift @@ -20,7 +20,7 @@ struct ChatterboxHeroView: View { .foregroundStyle(.secondary) Label(targetDescription, systemImage: "laptopcomputer") - .font(.caption) + .font(.callout) .foregroundStyle(.secondary) } .padding(.vertical, 6) diff --git a/CoreAILab/Features/Chatterbox/ChatterboxModelSection.swift b/CoreAILab/Features/Chatterbox/ChatterboxModelSection.swift index 5e4eab3..3b1e154 100644 --- a/CoreAILab/Features/Chatterbox/ChatterboxModelSection.swift +++ b/CoreAILab/Features/Chatterbox/ChatterboxModelSection.swift @@ -4,12 +4,9 @@ struct ChatterboxModelSection: View { let state: ChatterboxModelState var body: some View { - Section("Bundled Core AI Model") { + Section { Label(state.title, systemImage: state.systemImage) - - Text(state.detail) - .font(.subheadline) - .foregroundStyle(.secondary) + .help(state.detail) if case .ready(let inspection) = state { LabeledContent("Model bundle", value: inspection.formattedTotalSize) @@ -21,6 +18,8 @@ struct ChatterboxModelSection: View { LabeledContent("Author", value: inspection.author) } } + } header: { + Label("Bundled Core AI Model", systemImage: "shippingbox") } } } diff --git a/CoreAILab/Features/Chatterbox/ChatterboxPipelineSection.swift b/CoreAILab/Features/Chatterbox/ChatterboxPipelineSection.swift index d46aa74..771d58b 100644 --- a/CoreAILab/Features/Chatterbox/ChatterboxPipelineSection.swift +++ b/CoreAILab/Features/Chatterbox/ChatterboxPipelineSection.swift @@ -11,7 +11,7 @@ struct ChatterboxPipelineSection: View { } var body: some View { - Section("Native Pipeline") { + Section { ForEach(inspection?.assets ?? []) { asset in HStack(alignment: .firstTextBaseline) { Image(systemName: isReady(asset.stage) @@ -20,17 +20,13 @@ struct ChatterboxPipelineSection: View { .foregroundStyle(isReady(asset.stage) ? .green : .secondary) .accessibilityHidden(true) - VStack(alignment: .leading, spacing: 2) { - Text(asset.displayName) - Text(asset.detail) - .font(.caption) - .foregroundStyle(.secondary) - } + Text(asset.displayName) + .help(asset.detail) Spacer() Text(asset.formattedSize) - .font(.caption.monospacedDigit()) + .font(.callout.monospacedDigit()) .foregroundStyle(.secondary) } .accessibilityElement(children: .combine) @@ -38,7 +34,7 @@ struct ChatterboxPipelineSection: View { switch state { case .preparing: - ProgressView("Loading the recipe contract") + ProgressView("Loading the recipe contract…") case .failed: Label("Pipeline details are unavailable", systemImage: "xmark.octagon") .foregroundStyle(.red) @@ -49,9 +45,8 @@ struct ChatterboxPipelineSection: View { EmptyView() } - Text(detail) - .font(.caption) - .foregroundStyle(.secondary) + } header: { + Label("Native Pipeline", systemImage: "point.3.connected.trianglepath.dotted") } } @@ -59,18 +54,4 @@ struct ChatterboxPipelineSection: View { inspection?.contractValidation.presentStages.contains(stage) == true } - private var detail: String { - switch state { - case .notLoaded: - "The app has not started validating the bundled recipe." - case .preparing: - "The app is verifying every bundled asset and function before enabling generation." - case .ready(let inspection): - inspection.contractValidation.isComplete - ? "Text tokenization, autoregressive T3 decoding, S3Gen, and waveform synthesis all run locally." - : "The recipe is incomplete; generation remains disabled." - case .failed: - "Model preparation failed, so the pipeline contract could not be verified." - } - } } diff --git a/CoreAILab/Features/Chatterbox/ChatterboxPresentedError.swift b/CoreAILab/Features/Chatterbox/ChatterboxPresentedError.swift index 0e02110..77326de 100644 --- a/CoreAILab/Features/Chatterbox/ChatterboxPresentedError.swift +++ b/CoreAILab/Features/Chatterbox/ChatterboxPresentedError.swift @@ -2,5 +2,6 @@ import Foundation struct ChatterboxPresentedError: Identifiable { let id = UUID() + let title: String let message: String } diff --git a/CoreAILab/Features/Chatterbox/ChatterboxPromptSection.swift b/CoreAILab/Features/Chatterbox/ChatterboxPromptSection.swift index d899095..cfebb0d 100644 --- a/CoreAILab/Features/Chatterbox/ChatterboxPromptSection.swift +++ b/CoreAILab/Features/Chatterbox/ChatterboxPromptSection.swift @@ -4,13 +4,14 @@ struct ChatterboxPromptSection: View { @Binding var prompt: String var body: some View { - Section("Speech") { + Section { TextField("What should Chatterbox say?", text: $prompt, axis: .vertical) .lineLimit(4...) - - Text("Expressive tags such as [laugh], [chuckle], [sigh], and [gasp] stay in the text. One generation supports about 10 seconds of speech.") - .font(.caption) - .foregroundStyle(.secondary) + .help( + "Expressive tags such as [laugh], [chuckle], [sigh], and [gasp] stay in the text. One generation supports about 10 seconds of speech." + ) + } header: { + Label("Speech", systemImage: "text.quote") } } } diff --git a/CoreAILab/Features/Chatterbox/ChatterboxWorkspaceModel.swift b/CoreAILab/Features/Chatterbox/ChatterboxWorkspaceModel.swift index 6922270..9d0adee 100644 --- a/CoreAILab/Features/Chatterbox/ChatterboxWorkspaceModel.swift +++ b/CoreAILab/Features/Chatterbox/ChatterboxWorkspaceModel.swift @@ -5,12 +5,12 @@ import Observation @MainActor @Observable final class ChatterboxWorkspaceModel { - var prompt = "Oh, that's hilarious! [chuckle] This voice is running entirely on your Mac with Core AI." + var prompt = "Oh, that's hilarious! [chuckle] This voice was generated locally with Core AI." var modelState = ChatterboxModelState.notLoaded var generatedResult: ChatterboxGenerationResult? var isWorking = false var isPlaying = false - var statusMessage = "Preparing Core AI" + var statusMessage = "Preparing Core AI…" var presentedError: ChatterboxPresentedError? private(set) var recipeManifest: CoreAIRecipeManifest? @@ -34,6 +34,15 @@ final class ChatterboxWorkspaceModel { return inspection } + var isShowingError: Bool { + get { presentedError != nil } + set { + if !newValue { + presentedError = nil + } + } + } + var canGenerate: Bool { guard let inspection else { return false @@ -43,6 +52,14 @@ final class ChatterboxWorkspaceModel { && !isWorking } + var workingActionTitle: String { + if case .preparing = modelState { + "Preparing Models…" + } else { + "Generating Speech…" + } + } + func prepare() async { guard !hasPrepared else { return @@ -55,13 +72,15 @@ final class ChatterboxWorkspaceModel { let manifest = try await engine.bundledRecipeManifest() recipeManifest = manifest let targetName = manifest.defaultTarget?.displayName ?? "selected target" - statusMessage = "Specializing \(manifest.pipeline.stages.count) models for \(targetName)" - modelState = .ready(try await engine.prepareBundledModels()) + statusMessage = "Specializing \(manifest.pipeline.stages.count) models for \(targetName)…" + let inspection = try await engine.prepareBundledModels() + modelState = .ready(inspection) + statusMessage = "Ready to generate speech." } catch { recipeManifest = nil modelState = .failed(error.localizedDescription) - statusMessage = "Core AI preparation failed" - present(error) + statusMessage = "Core AI preparation failed." + present(error, title: "Couldn't Prepare Chatterbox") } isWorking = false } @@ -83,7 +102,7 @@ final class ChatterboxWorkspaceModel { private func synthesize() async { stopPlayback() isWorking = true - statusMessage = "Generating speech entirely with Core AI" + statusMessage = "Generating speech with Core AI…" generatedResult = nil do { @@ -91,9 +110,11 @@ final class ChatterboxWorkspaceModel { ChatterboxGenerationRequest(text: prompt) ) generatedResult = result + statusMessage = "Generated \(result.audioDuration.formatted(.number.precision(.fractionLength(1)))) seconds of speech in \(result.elapsedTime.formatted(.number.precision(.fractionLength(1)))) seconds." play(result) } catch { - present(error) + statusMessage = "Speech generation failed." + present(error, title: "Couldn't Generate Speech") } isWorking = false } @@ -117,7 +138,9 @@ final class ChatterboxWorkspaceModel { self?.isPlaying = false } } catch { - present(error) + isPlaying = false + statusMessage = "Speech is ready, but playback couldn't start." + present(error, title: "Couldn't Play Speech") } } @@ -129,8 +152,9 @@ final class ChatterboxWorkspaceModel { isPlaying = false } - private func present(_ error: Error) { + private func present(_ error: Error, title: String) { presentedError = ChatterboxPresentedError( + title: title, message: error.localizedDescription ) } diff --git a/CoreAILab/Features/Chatterbox/ChatterboxWorkspaceView.swift b/CoreAILab/Features/Chatterbox/ChatterboxWorkspaceView.swift index 7d7adf4..dedeeb3 100644 --- a/CoreAILab/Features/Chatterbox/ChatterboxWorkspaceView.swift +++ b/CoreAILab/Features/Chatterbox/ChatterboxWorkspaceView.swift @@ -8,8 +8,6 @@ struct ChatterboxWorkspaceView: View { NavigationStack { Form { - ChatterboxHeroView(manifest: model.recipeManifest) - ChatterboxModelSection(state: model.modelState) ChatterboxPromptSection(prompt: $model.prompt) @@ -19,6 +17,7 @@ struct ChatterboxWorkspaceView: View { ChatterboxGenerationSection( canGenerate: model.canGenerate, isWorking: model.isWorking, + workingActionTitle: model.workingActionTitle, statusMessage: model.statusMessage, result: model.generatedResult, isPlaying: model.isPlaying, @@ -28,15 +27,29 @@ struct ChatterboxWorkspaceView: View { } .formStyle(.grouped) .navigationTitle(model.recipeManifest?.displayName ?? "Text to Speech") + .toolbar { +#if os(macOS) + ToolbarItem(placement: .primaryAction) { + Button( + "Generate Speech", + systemImage: "play.circle.fill", + action: model.generate + ) + .disabled(!model.canGenerate) + .help(model.statusMessage) + } +#endif + } .task { await model.prepare() } - .alert(item: $model.presentedError) { presentedError in - Alert( - title: Text("Chatterbox Core AI"), - message: Text(presentedError.message), - dismissButton: .default(Text("OK")) - ) + .alert( + model.presentedError?.title ?? "Couldn't Complete the Request", + isPresented: $model.isShowingError + ) { + Button("Dismiss", role: .cancel) {} + } message: { + Text(model.presentedError?.message ?? "Try again.") } } } diff --git a/CoreAILab/Features/Conversion/CoreAIConversionCommandView.swift b/CoreAILab/Features/Conversion/CoreAIConversionCommandView.swift index 8d653f1..7238a4c 100644 --- a/CoreAILab/Features/Conversion/CoreAIConversionCommandView.swift +++ b/CoreAILab/Features/Conversion/CoreAIConversionCommandView.swift @@ -16,10 +16,9 @@ struct CoreAIConversionCommandView: View { .padding(.vertical, 4) } .scrollIndicators(.visible) - - Text("Displayed for evidence only. Core AI Lab passes these arguments directly without invoking a shell.") - .font(.callout) - .foregroundStyle(.secondary) + .help( + "Displayed for evidence only. Core AI Lab passes these arguments directly without invoking a shell." + ) } } } diff --git a/CoreAILab/Features/Conversion/CoreAIConversionLogView.swift b/CoreAILab/Features/Conversion/CoreAIConversionLogView.swift index 9eee8fe..6c83683 100644 --- a/CoreAILab/Features/Conversion/CoreAIConversionLogView.swift +++ b/CoreAILab/Features/Conversion/CoreAIConversionLogView.swift @@ -9,12 +9,11 @@ struct CoreAIConversionLogView: View { ScrollView { LazyVStack(alignment: .leading, spacing: 3) { if entries.isEmpty { - Label( - "Converter output will appear here", + ContentUnavailableView( + "No Converter Output", systemImage: "text.alignleft" ) - .foregroundStyle(.secondary) - .padding() + .frame(maxWidth: .infinity, minHeight: 200) } else { ForEach(entries) { entry in Text(entry.message) @@ -27,7 +26,7 @@ struct CoreAIConversionLogView: View { .padding() .textSelection(.enabled) } - .background(.black.opacity(0.04)) + .background(.secondary.opacity(0.08)) .onChange(of: entries.count) { guard let lastID = entries.last?.id else { return } proxy.scrollTo(lastID, anchor: .bottom) diff --git a/CoreAILab/Features/Conversion/CoreAIConversionSetupView.swift b/CoreAILab/Features/Conversion/CoreAIConversionSetupView.swift index 69bc4fa..4ed1357 100644 --- a/CoreAILab/Features/Conversion/CoreAIConversionSetupView.swift +++ b/CoreAILab/Features/Conversion/CoreAIConversionSetupView.swift @@ -9,7 +9,7 @@ struct CoreAIConversionSetupView: View { var body: some View { Form { - Section("Recipe") { + Section { Picker("Model", selection: $workspace.selectedModelID) { ForEach(workspace.groups) { group in Section(group.category.rawValue) { @@ -33,10 +33,12 @@ struct CoreAIConversionSetupView: View { } } } + } header: { + Label("Recipe", systemImage: "shippingbox") } .disabled(configurationIsLocked) - Section("Workspace") { + Section { CoreAIConversionPathRow( title: "Apple repository", url: workspace.repositoryURL, @@ -60,27 +62,27 @@ struct CoreAIConversionSetupView: View { actionTitle: "Choose uv Executable", action: chooseUVExecutable ) + } header: { + Label("Workspace", systemImage: "folder") } .disabled(configurationIsLocked) - Section("Options") { + Section { Toggle( "Overwrite matching artifacts", isOn: $workspace.overwriteExistingArtifacts ) - Text("Source weights remain in the upstream cache. Core AI Lab does not redistribute or relicense them.") - .foregroundStyle(.secondary) + .help("Source weights remain in the upstream cache and keep their upstream license.") + } header: { + Label("Options", systemImage: "switch.2") } .disabled(configurationIsLocked) - Section("Environment") { + Section { if let report = workspace.environmentReport { ForEach(report.checks) { check in CoreAIConversionEnvironmentCheckView(check: check) } - } else { - Text("Run the environment check before converting.") - .foregroundStyle(.secondary) } Button( @@ -89,30 +91,10 @@ struct CoreAIConversionSetupView: View { action: checkEnvironment ) .disabled(workspace.phase.isBusy) + } header: { + Label("Environment", systemImage: "checkmark.shield") } - Section { - if workspace.canCancelConversion { - Button( - "Cancel Conversion", - systemImage: "stop.fill", - role: .destructive, - action: workspace.cancelConversion - ) - .keyboardShortcut(.cancelAction) - } else { - Button( - "Start Conversion", - systemImage: "play.fill", - action: workspace.startConversion - ) - .buttonStyle(.borderedProminent) - .keyboardShortcut(.return, modifiers: .command) - .disabled(!workspace.canStartConversion) - } - } footer: { - Text("The first run can create a Python environment and download many gigabytes. The evidence pane keeps the original converter output visible.") - } } .formStyle(.grouped) } diff --git a/CoreAILab/Features/Conversion/CoreAIConversionStatusView.swift b/CoreAILab/Features/Conversion/CoreAIConversionStatusView.swift index 46080d5..3349ee3 100644 --- a/CoreAILab/Features/Conversion/CoreAIConversionStatusView.swift +++ b/CoreAILab/Features/Conversion/CoreAIConversionStatusView.swift @@ -22,9 +22,6 @@ struct CoreAIConversionStatusView: View { } } - Text(statusMessage) - .foregroundStyle(.secondary) - HStack(spacing: 16) { if let processIdentifier { Label("PID \(processIdentifier)", systemImage: "terminal") @@ -39,7 +36,9 @@ struct CoreAIConversionStatusView: View { .font(.callout.monospacedDigit()) .foregroundStyle(.secondary) } + .help(statusMessage) .accessibilityElement(children: .contain) + .accessibilityHint(statusMessage) .accessibilityAddTraits(.updatesFrequently) } } diff --git a/CoreAILab/Features/Conversion/CoreAIConversionWorkspaceView.swift b/CoreAILab/Features/Conversion/CoreAIConversionWorkspaceView.swift index 1b08182..536c123 100644 --- a/CoreAILab/Features/Conversion/CoreAIConversionWorkspaceView.swift +++ b/CoreAILab/Features/Conversion/CoreAIConversionWorkspaceView.swift @@ -43,6 +43,28 @@ struct CoreAIConversionWorkspaceView: View { } } .navigationTitle("Convert") + .toolbar { + ToolbarItem(placement: .primaryAction) { + if workspace.canCancelConversion { + Button( + "Cancel Conversion", + systemImage: "stop.fill", + role: .cancel, + action: workspace.cancelConversion + ) + .keyboardShortcut(.cancelAction) + } else { + Button( + "Start Conversion", + systemImage: "play.fill", + action: workspace.startConversion + ) + .disabled(!workspace.canStartConversion) + .help(workspace.statusMessage) + .keyboardShortcut(.return, modifiers: .command) + } + } + } .navigationDestination(for: CoreAIConversionArtifact.self) { artifact in CoreAIConversionArtifactDestinationView(artifact: artifact) } @@ -64,10 +86,9 @@ struct CoreAIConversionWorkspaceView: View { ) { result in handleUVSelection(result) } - .alert("Conversion Error", isPresented: $workspace.isShowingError) { - Button("OK", role: .cancel) {} + .alert("Couldn't Complete the Conversion", isPresented: $workspace.isShowingError) { } message: { - Text(workspace.errorMessage ?? "The conversion could not be completed.") + Text(workspace.errorMessage ?? "Review the environment checks and try again.") } .sheet(item: $artifactToStore) { artifact in CoreAIArtifactProjectPickerView(artifactURL: artifact.url) @@ -100,7 +121,7 @@ struct CoreAIConversionWorkspaceView: View { await workspace.refreshEnvironment() } case .failure(let error): - workspace.presentImportError(error) + presentSelectionError(error) } } @@ -112,7 +133,7 @@ struct CoreAIConversionWorkspaceView: View { await workspace.refreshEnvironment() } case .failure(let error): - workspace.presentImportError(error) + presentSelectionError(error) } } @@ -124,6 +145,12 @@ struct CoreAIConversionWorkspaceView: View { await workspace.refreshEnvironment() } case .failure(let error): + presentSelectionError(error) + } + } + + private func presentSelectionError(_ error: any Error) { + if (error as? CocoaError)?.code != .userCancelled { workspace.presentImportError(error) } } diff --git a/CoreAILab/Features/DeviceLab/CoreAIDeviceDiagnosticsView.swift b/CoreAILab/Features/DeviceLab/CoreAIDeviceDiagnosticsView.swift index e742f3d..5fa6ad9 100644 --- a/CoreAILab/Features/DeviceLab/CoreAIDeviceDiagnosticsView.swift +++ b/CoreAILab/Features/DeviceLab/CoreAIDeviceDiagnosticsView.swift @@ -4,16 +4,16 @@ struct CoreAIDeviceDiagnosticsView: View { let diagnostics: [CoreAIDeviceDiagnostic] var body: some View { - Section("Authoring and Compatibility") { + Section { ForEach(diagnostics) { diagnostic in - LabeledContent { - Text(diagnostic.detail) - .foregroundStyle(.secondary) - } label: { + VStack(alignment: .leading) { Label( diagnostic.title, systemImage: diagnostic.severity.systemImage ) + Text(diagnostic.detail) + .font(.footnote) + .foregroundStyle(.secondary) } .accessibilityElement(children: .ignore) .accessibilityLabel( @@ -21,6 +21,8 @@ struct CoreAIDeviceDiagnosticsView: View { ) .accessibilityValue(diagnostic.detail) } + } header: { + Label("Compatibility Checks", systemImage: "checkmark.shield") } } } diff --git a/CoreAILab/Features/DeviceLab/CoreAIDeviceEvidenceView.swift b/CoreAILab/Features/DeviceLab/CoreAIDeviceEvidenceView.swift index ae941ce..354ca56 100644 --- a/CoreAILab/Features/DeviceLab/CoreAIDeviceEvidenceView.swift +++ b/CoreAILab/Features/DeviceLab/CoreAIDeviceEvidenceView.swift @@ -5,7 +5,7 @@ struct CoreAIDeviceEvidenceView: View { @Binding var isImportingEvidence: Bool var body: some View { - Section("Physical Evidence") { + Section { Button( "Import Runner Evidence", systemImage: "square.and.arrow.down", @@ -19,7 +19,7 @@ struct CoreAIDeviceEvidenceView: View { if let error = workspace.importErrorMessage { Label(error, systemImage: "xmark.octagon") - .foregroundStyle(.secondary) + .foregroundStyle(.red) } if let evidence = workspace.importedEvidence { @@ -31,36 +31,68 @@ struct CoreAIDeviceEvidenceView: View { : evidence.device.modelIdentifier ) LabeledContent("iOS", value: evidence.device.operatingSystemVersion) - LabeledContent("Artifact", value: evidence.artifact.identifier) - LabeledContent("Configuration", value: evidence.configuration.identifier) + LabeledContent("Artifact") { + Text(evidence.artifact.identifier) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } + LabeledContent("Configuration") { + Text(evidence.configuration.identifier) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } LabeledContent( "Specialization", - value: evidence.specialization.status.rawValue + value: displayName(evidence.specialization.status) ) - LabeledContent("Inference", value: evidence.inference.status.rawValue) - LabeledContent("Energy", value: evidence.energy.availability.rawValue) LabeledContent( - "Execution placement", - value: evidence.placement.availability.rawValue + "Inference", + value: displayName(evidence.inference.status) + ) + LabeledContent( + "Energy", + value: displayName(evidence.energy.availability) ) - Text( - "Artifact and configuration SHA-256 identities are retained in the imported JSON." + LabeledContent( + "Execution placement", + value: displayName(evidence.placement.availability) ) - .font(.subheadline) - .foregroundStyle(.secondary) } else { ContentUnavailableView( "No Device Evidence", - systemImage: "iphone.slash", - description: Text( - "Run the physical harness or its dry run with --evidence-json, then import that file." - ) + systemImage: "iphone.slash" ) + .help("Run the physical harness with --evidence-json, then import that file.") } + } header: { + Label("Physical Evidence", systemImage: "doc.text.magnifyingglass") } + .help("Imported JSON retains artifact and configuration SHA-256 identities.") } private func beginImport() { isImportingEvidence = true } + + private func displayName(_ status: CoreAIDeviceTrialStatus) -> String { + switch status { + case .notRun: + "Not run" + case .succeeded: + "Succeeded" + case .failed: + "Failed" + } + } + + private func displayName(_ availability: CoreAIDeviceMetricAvailability) -> String { + switch availability { + case .unavailable: + "Unavailable" + case .observed: + "Observed" + } + } } diff --git a/CoreAILab/Features/DeviceLab/CoreAIDeviceLabView.swift b/CoreAILab/Features/DeviceLab/CoreAIDeviceLabView.swift index a0e7fc4..91a5ee5 100644 --- a/CoreAILab/Features/DeviceLab/CoreAIDeviceLabView.swift +++ b/CoreAILab/Features/DeviceLab/CoreAIDeviceLabView.swift @@ -7,13 +7,6 @@ struct CoreAIDeviceLabView: View { var body: some View { Form { - Section { - Text( - "Author an iPhone target, plan its asset delivery, and import evidence from the physical runner. Preferences remain separate from measured execution placement." - ) - .foregroundStyle(.secondary) - } - CoreAIDeviceTargetAuthoringView(workspace: workspace) CoreAIDeviceStoragePlanView(workspace: workspace) CoreAIDeviceDiagnosticsView(diagnostics: workspace.diagnostics) @@ -22,6 +15,10 @@ struct CoreAIDeviceLabView: View { isImportingEvidence: $isImportingEvidence ) } + .formStyle(.grouped) + .help( + "Author an iPhone target, plan asset delivery, and import physical-runner evidence." + ) .navigationTitle("Device Lab") .fileImporter( isPresented: $isImportingEvidence, @@ -37,7 +34,9 @@ struct CoreAIDeviceLabView: View { guard let url = urls.first else { return } workspace.importEvidence(from: url) case .failure(let error): - workspace.reportImportFailure(error) + if (error as? CocoaError)?.code != .userCancelled { + workspace.reportImportFailure(error) + } } } } diff --git a/CoreAILab/Features/DeviceLab/CoreAIDeviceStoragePlanView.swift b/CoreAILab/Features/DeviceLab/CoreAIDeviceStoragePlanView.swift index adf94d4..c9f0100 100644 --- a/CoreAILab/Features/DeviceLab/CoreAIDeviceStoragePlanView.swift +++ b/CoreAILab/Features/DeviceLab/CoreAIDeviceStoragePlanView.swift @@ -4,7 +4,7 @@ struct CoreAIDeviceStoragePlanView: View { @Bindable var workspace: CoreAIDeviceLabWorkspaceModel var body: some View { - Section("Asset Delivery") { + Section { Picker("Model delivery", selection: $workspace.modelDeliveryMode) { ForEach(CoreAIAssetDeliveryMode.allCases, id: \.self) { mode in Text(mode.title).tag(mode) @@ -43,7 +43,7 @@ struct CoreAIDeviceStoragePlanView: View { if let error = workspace.storagePlanErrorMessage { Label(error, systemImage: "xmark.octagon") - .foregroundStyle(.secondary) + .foregroundStyle(.red) } else if let plan = workspace.storagePlan { LabeledContent("App download") { Text( @@ -77,6 +77,8 @@ struct CoreAIDeviceStoragePlanView: View { } } } + } header: { + Label("Asset Delivery", systemImage: "shippingbox") } } } diff --git a/CoreAILab/Features/DeviceLab/CoreAIDeviceTargetAuthoringView.swift b/CoreAILab/Features/DeviceLab/CoreAIDeviceTargetAuthoringView.swift index 1e8d2bb..d7ba0f7 100644 --- a/CoreAILab/Features/DeviceLab/CoreAIDeviceTargetAuthoringView.swift +++ b/CoreAILab/Features/DeviceLab/CoreAIDeviceTargetAuthoringView.swift @@ -4,13 +4,14 @@ struct CoreAIDeviceTargetAuthoringView: View { @Bindable var workspace: CoreAIDeviceLabWorkspaceModel var body: some View { - Section("iPhone Target") { + Section { Picker("Compute preference", selection: $workspace.preferredComputeUnit) { ForEach(CoreAIComputeUnitPreference.allCases, id: \.self) { preference in Text(workspace.computeUnitTitle(preference)) .tag(preference) } } + .help("A compute preference does not prove execution placement.") Toggle( "Expect frequent reshapes", isOn: $workspace.expectsFrequentReshapes @@ -52,11 +53,8 @@ struct CoreAIDeviceTargetAuthoringView: View { "Leave input widths dynamic", isOn: $workspace.usesDynamicSequenceDimension ) - Text( - "A compute preference shapes specialization options. It is not an execution-placement measurement." - ) - .font(.subheadline) - .foregroundStyle(.secondary) + } header: { + Label("iPhone Target", systemImage: "iphone.gen3") } } } diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationAnalysisSection.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationAnalysisSection.swift index 448a50a..4992f35 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationAnalysisSection.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationAnalysisSection.swift @@ -1,73 +1,57 @@ import SwiftUI -enum SpeakerDiarizationAnalysisLayout: Equatable { - case stacked - case sideBySide - - static let minimumSideBySideWidth: CGFloat = 880 - - init(contentWidth: CGFloat) { - self = contentWidth >= Self.minimumSideBySideWidth ? .sideBySide : .stacked - } -} - struct SpeakerDiarizationAnalysisSection: View { private static let maximumContentWidth: CGFloat = 1_120 private static let horizontalMargin: CGFloat = 64 - let availableWidth: CGFloat let waveform: SpeakerDiarizationWaveform? let result: SpeakerDiarizationResult? let playheadTime: Double let activeTurnID: Int? - private var contentWidth: CGFloat { - min( - max(availableWidth - Self.horizontalMargin, 0), - Self.maximumContentWidth - ) - } - - private var layout: SpeakerDiarizationAnalysisLayout { - SpeakerDiarizationAnalysisLayout(contentWidth: contentWidth) - } - var body: some View { Section { - Group { - if layout == .sideBySide { - HStack(alignment: .top, spacing: 20) { - SpeakerDiarizationTimelineView( - waveform: waveform, - result: result, - playheadTime: playheadTime - ) - .frame(maxWidth: .infinity, alignment: .topLeading) - Divider() - SpeakerDiarizationResultsView( - result: result, - activeTurnID: activeTurnID - ) - .frame(maxWidth: .infinity, alignment: .topLeading) - } - } else { - VStack(alignment: .leading, spacing: 20) { - SpeakerDiarizationTimelineView( - waveform: waveform, - result: result, - playheadTime: playheadTime - ) - .frame(maxWidth: .infinity, alignment: .topLeading) - Divider() - SpeakerDiarizationResultsView( - result: result, - activeTurnID: activeTurnID - ) - .frame(maxWidth: .infinity, alignment: .topLeading) - } + ViewThatFits(in: .horizontal) { + HStack(alignment: .top) { + SpeakerDiarizationTimelineView( + waveform: waveform, + result: result, + playheadTime: playheadTime + ) + .frame(minWidth: 360, maxWidth: .infinity, alignment: .topLeading) + + Divider() + + SpeakerDiarizationResultsView( + result: result, + activeTurnID: activeTurnID + ) + .frame(minWidth: 360, maxWidth: .infinity, alignment: .topLeading) } + + VStack(alignment: .leading) { + SpeakerDiarizationTimelineView( + waveform: waveform, + result: result, + playheadTime: playheadTime + ) + .frame(maxWidth: .infinity, alignment: .topLeading) + + Divider() + + SpeakerDiarizationResultsView( + result: result, + activeTurnID: activeTurnID + ) + .frame(maxWidth: .infinity, alignment: .topLeading) + } + } + .containerRelativeFrame(.horizontal) { length, _ in + min( + max(length - Self.horizontalMargin, 0), + Self.maximumContentWidth + ) } - .frame(width: contentWidth, alignment: .topLeading) } } } diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationImportControls.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationImportControls.swift index f8f10ce..1f8c2e9 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationImportControls.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationImportControls.swift @@ -3,7 +3,8 @@ import SwiftUI struct SpeakerDiarizationImportControls: View { let layout: ControlsLayout let canRunDiarization: Bool - let isBusy: Bool + let canImportModel: Bool + let canImportMedia: Bool let importModelAction: () -> Void let importMediaAction: () -> Void let runAction: () -> Void @@ -11,12 +12,14 @@ struct SpeakerDiarizationImportControls: View { var body: some View { layout { Button("Choose CAM++", systemImage: "shippingbox", action: importModelAction) - .disabled(isBusy) + .disabled(!canImportModel) Button("Choose Audio or Video", systemImage: "waveform", action: importMediaAction) - .disabled(isBusy) + .disabled(!canImportMedia) +#if !os(macOS) Button("Run Diarization", systemImage: "person.2.wave.2", action: runAction) .buttonStyle(.borderedProminent) .disabled(!canRunDiarization) +#endif } } } diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationImportSection.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationImportSection.swift index 993d037..ccb54fe 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationImportSection.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationImportSection.swift @@ -2,18 +2,20 @@ import SwiftUI struct SpeakerDiarizationImportSection: View { let canRunDiarization: Bool - let isBusy: Bool + let canImportModel: Bool + let canImportMedia: Bool let importModelAction: () -> Void let importMediaAction: () -> Void let runAction: () -> Void var body: some View { - Section("Inputs") { + Section { ViewThatFits(in: .horizontal) { SpeakerDiarizationImportControls( layout: HStackLayout(), canRunDiarization: canRunDiarization, - isBusy: isBusy, + canImportModel: canImportModel, + canImportMedia: canImportMedia, importModelAction: importModelAction, importMediaAction: importMediaAction, runAction: runAction @@ -21,15 +23,18 @@ struct SpeakerDiarizationImportSection: View { SpeakerDiarizationImportControls( layout: VStackLayout(alignment: .leading), canRunDiarization: canRunDiarization, - isBusy: isBusy, + canImportModel: canImportModel, + canImportMedia: canImportMedia, importModelAction: importModelAction, importMediaAction: importMediaAction, runAction: runAction ) } - - Text("The bundled Apache-2.0 CAM++ model runs through Core AI after 16 kHz decode, energy segmentation, and six-second feature preparation; cosine clustering produces anonymous speaker turns.") - .foregroundStyle(.secondary) + } header: { + Label("Inputs", systemImage: "waveform.and.mic") } + .help( + "Core AI runs the bundled CAM++ model after 16 kHz decode, energy segmentation, and feature preparation." + ) } } diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationResultsView.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationResultsView.swift index 0bd2506..b6fa1e6 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationResultsView.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationResultsView.swift @@ -79,8 +79,7 @@ struct SpeakerDiarizationResultsView: View { } else { ContentUnavailableView( "No Speaker Turns Yet", - systemImage: "person.2.slash", - description: Text("Choose media, then run the bundled CAM++ diarizer.") + systemImage: "person.2.slash" ) .frame(maxWidth: .infinity, minHeight: 180) } diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationStatusSection.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationStatusSection.swift index 987ef1b..5380065 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationStatusSection.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationStatusSection.swift @@ -31,12 +31,15 @@ struct SpeakerDiarizationStatusSection: View { ) } } - Label(statusMessage, systemImage: isBusy ? "hourglass" : "waveform.badge.mic") - .foregroundStyle(isBusy ? .primary : .secondary) + if isBusy { + ProgressView(statusMessage) + .accessibilityAddTraits(.updatesFrequently) + } } header: { - Label("Speaker Diarization Lab", systemImage: "person.wave.2") - } footer: { - Text("The bundled CAM++ asset is Apache-2.0. This experimental batch engine assigns anonymous labels, not real identities, and energy segmentation does not detect overlapping speakers.") + Label("Speaker Diarization", systemImage: "person.wave.2") } + .help( + "\(statusMessage) The Apache-2.0 CAM++ engine uses anonymous labels and does not detect overlapping speakers." + ) } } diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationTimelineView.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationTimelineView.swift index 23b8137..c765e5c 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationTimelineView.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationTimelineView.swift @@ -45,8 +45,7 @@ struct SpeakerDiarizationTimelineView: View { } else { ContentUnavailableView( "No Media Selected", - systemImage: "waveform", - description: Text("Choose an audio or video file to build the first timeline.") + systemImage: "waveform" ) .frame(maxWidth: .infinity, minHeight: 180) } diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationTurnRow.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationTurnRow.swift index 0ee3482..ba33db8 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationTurnRow.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationTurnRow.swift @@ -6,7 +6,7 @@ struct SpeakerDiarizationTurnRow: View { var body: some View { LabeledContent { - VStack(alignment: .trailing) { + HStack { Text(timeRange) .monospacedDigit() Text(clusterEvidence) @@ -20,11 +20,10 @@ struct SpeakerDiarizationTurnRow: View { systemImage: isActive ? "speaker.wave.3.fill" : "person.wave.2" ) if isActive { - Text("Now") - .font(.caption.weight(.semibold)) - .padding(.horizontal, 8) - .padding(.vertical, 3) - .background(.orange.opacity(0.18), in: .capsule) + Label("Now", systemImage: "play.fill") + .font(.footnote) + .bold() + .foregroundStyle(.orange) } } } diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationWatcherSection.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationWatcherSection.swift index 3d01993..282c6d4 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationWatcherSection.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationWatcherSection.swift @@ -21,8 +21,7 @@ struct SpeakerDiarizationWatcherSection: View { } else { ContentUnavailableView( "Audio Watcher", - systemImage: "waveform.circle", - description: Text("Playback still drives the same live playhead and active speaker state.") + systemImage: "waveform.circle" ) } @@ -40,16 +39,14 @@ struct SpeakerDiarizationWatcherSection: View { Button("Restart", systemImage: "backward.end.fill", action: restart) } - Text("The watcher synchronizes playback with the completed batch timeline. It does not claim streaming inference.") - .foregroundStyle(.secondary) } else { ContentUnavailableView( "No Media to Watch", - systemImage: "play.rectangle", - description: Text("Import audio or video to enable synchronized playback.") + systemImage: "play.rectangle" ) } } + .help("Playback follows the completed batch timeline; it is not streaming inference.") } private func positionText(for summary: SpeakerDiarizationMediaSummary) -> String { diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationWorkspaceModel.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationWorkspaceModel.swift index 286a90d..95c9cc7 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationWorkspaceModel.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationWorkspaceModel.swift @@ -73,7 +73,7 @@ final class SpeakerDiarizationWorkspaceModel { ? "CAM++ is ready. Choose media or run diarization." : "Media and CAM++ are ready for batch diarization." } catch is CancellationError { - statusMessage = "Model import cancelled." + statusMessage = "Model import canceled." } catch { present(error) } @@ -85,10 +85,6 @@ final class SpeakerDiarizationWorkspaceModel { diarizationTask = nil analysisGeneration += 1 let generation = analysisGeneration - mediaURL = nil - mediaSummary = nil - waveform = nil - result = nil isRunningDiarization = false isAnalyzingMedia = true clearError() @@ -137,6 +133,7 @@ final class SpeakerDiarizationWorkspaceModel { mediaURL = url mediaSummary = analysis.summary waveform = analysis.waveform + result = nil clearError() statusMessage = modelInfo == nil ? "Media is ready. Choose a compatible CAM++ model to diarize it." diff --git a/CoreAILab/Features/Diarization/SpeakerDiarizationWorkspaceView.swift b/CoreAILab/Features/Diarization/SpeakerDiarizationWorkspaceView.swift index 6940e12..138a1f1 100644 --- a/CoreAILab/Features/Diarization/SpeakerDiarizationWorkspaceView.swift +++ b/CoreAILab/Features/Diarization/SpeakerDiarizationWorkspaceView.swift @@ -11,23 +11,26 @@ struct SpeakerDiarizationWorkspaceView: View { let activeTurn = workspace.result?.turn(at: watcher.currentTime) NavigationStack { - GeometryReader { geometry in - Form { - SpeakerDiarizationStatusSection( - modelInfo: workspace.modelInfo, - summary: workspace.mediaSummary, - statusMessage: workspace.statusMessage, - isBusy: workspace.isBusy - ) + Form { + SpeakerDiarizationStatusSection( + modelInfo: workspace.modelInfo, + summary: workspace.mediaSummary, + statusMessage: workspace.statusMessage, + isBusy: workspace.isBusy + ) - SpeakerDiarizationImportSection( - canRunDiarization: workspace.canRunDiarization, - isBusy: workspace.isBusy, - importModelAction: importModel, - importMediaAction: importMedia, - runAction: workspace.startDiarization - ) + SpeakerDiarizationImportSection( + canRunDiarization: workspace.canRunDiarization, + canImportModel: !workspace.isLoadingModel + && !workspace.isRunningDiarization, + canImportMedia: !workspace.isAnalyzingMedia + && !workspace.isRunningDiarization, + importModelAction: importModel, + importMediaAction: importMedia, + runAction: workspace.startDiarization + ) + if workspace.mediaSummary != nil { SpeakerDiarizationWatcherSection( summary: workspace.mediaSummary, player: watcher.player, @@ -39,16 +42,28 @@ struct SpeakerDiarizationWorkspaceView: View { ) SpeakerDiarizationAnalysisSection( - availableWidth: geometry.size.width, waveform: workspace.waveform, result: workspace.result, playheadTime: watcher.currentTime, activeTurnID: activeTurn?.id ) } - .formStyle(.grouped) } + .formStyle(.grouped) .navigationTitle("Diarization") + .toolbar { +#if os(macOS) + ToolbarItem(placement: .primaryAction) { + Button( + "Run Diarization", + systemImage: "person.2.wave.2", + action: workspace.startDiarization + ) + .disabled(!workspace.canRunDiarization) + .help(workspace.statusMessage) + } +#endif + } .task { await workspace.prepareBundledModel() } @@ -71,9 +86,9 @@ struct SpeakerDiarizationWorkspaceView: View { ) { result in handleMediaImport(result) } - .alert("Diarization Lab Failed", isPresented: $workspace.isShowingError) { + .alert("Couldn't Separate the Speakers", isPresented: $workspace.isShowingError) { } message: { - Text(workspace.errorMessage ?? "The request could not be completed.") + Text(workspace.errorMessage ?? "Check the model and media file, then try again.") } } } @@ -91,7 +106,7 @@ struct SpeakerDiarizationWorkspaceView: View { case .success(let url): workspace.selectMedia(url) case .failure(let error): - workspace.presentImportError(error) + presentSelectionError(error) } } @@ -102,6 +117,12 @@ struct SpeakerDiarizationWorkspaceView: View { await workspace.loadModel(from: url) } case .failure(let error): + presentSelectionError(error) + } + } + + private func presentSelectionError(_ error: any Error) { + if (error as? CocoaError)?.code != .userCancelled { workspace.presentImportError(error) } } diff --git a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkActionsView.swift b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkActionsView.swift index f3fe52f..802f5c0 100644 --- a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkActionsView.swift +++ b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkActionsView.swift @@ -18,11 +18,15 @@ struct CoreAIFunctionBenchmarkActionsView: View { ) .buttonStyle(.borderedProminent) .disabled(!workspace.canBenchmark) + .help( + "Warmups are excluded. Measured runs reuse one deterministic input set and execute sequentially." + ) if workspace.phase == .benchmarking { Button( "Stop After Current Inference", systemImage: "stop.fill", + role: .cancel, action: workspace.stopBenchmarkAfterCurrentInference ) } diff --git a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkControlsView.swift b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkControlsView.swift index 7a23904..62c317f 100644 --- a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkControlsView.swift +++ b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkControlsView.swift @@ -31,8 +31,13 @@ struct CoreAIFunctionBenchmarkControlsView: View { } if let message = workspace.benchmarkStatusMessage { - Label(message, systemImage: "info.circle") - .foregroundStyle(.secondary) + if workspace.phase == .benchmarking { + ProgressView(message) + .accessibilityAddTraits(.updatesFrequently) + } else { + Label(message, systemImage: "info.circle") + .foregroundStyle(.secondary) + } } if CoreAIBuildConfiguration.current == .debug { @@ -43,11 +48,7 @@ struct CoreAIFunctionBenchmarkControlsView: View { .foregroundStyle(.orange) } } header: { - Text("Benchmark") - } footer: { - Text( - "Warmups are excluded. Measured runs reuse one function and one deterministic input set, execute sequentially, and remain visible individually. Stopping takes effect between Core AI inference calls." - ) + Label("Benchmark", systemImage: "gauge.with.dots.needle.67percent") } } } diff --git a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkResultsView.swift b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkResultsView.swift index d57d937..859f97a 100644 --- a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkResultsView.swift +++ b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionBenchmarkResultsView.swift @@ -5,13 +5,15 @@ struct CoreAIFunctionBenchmarkResultsView: View { let exportEvidence: (CoreAIFunctionBenchmarkReport) -> Void var body: some View { - Section("Benchmark History") { + Section { ForEach(reports) { report in CoreAIFunctionBenchmarkReportView( report: report, exportEvidence: exportEvidence ) } + } header: { + Label("Benchmark History", systemImage: "clock.arrow.circlepath") } } } diff --git a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionContractView.swift b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionContractView.swift index b1c80d3..e02bac8 100644 --- a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionContractView.swift +++ b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionContractView.swift @@ -4,7 +4,7 @@ struct CoreAIFunctionContractView: View { @Bindable var workspace: CoreAIFunctionWorkbenchWorkspaceModel var body: some View { - Section("Function") { + Section { Picker("Entry point", selection: $workspace.selectedFunctionName) { ForEach(workspace.contracts) { contract in Text(contract.name) @@ -23,6 +23,8 @@ struct CoreAIFunctionContractView: View { .foregroundStyle(.secondary) } } + } header: { + Label("Function", systemImage: "function") } } } diff --git a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionInputsView.swift b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionInputsView.swift index 28f3af9..5dc5487 100644 --- a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionInputsView.swift +++ b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionInputsView.swift @@ -5,10 +5,9 @@ struct CoreAIFunctionInputsView: View { let isDisabled: Bool var body: some View { - Section("Generated Inputs") { + Section { if drafts.isEmpty { - Text("This function has no generated tensor inputs.") - .foregroundStyle(.secondary) + Label("No Generated Inputs", systemImage: "minus.circle") } else { ForEach(drafts, id: \.name) { draft in CoreAIFunctionInputDraftView( @@ -17,6 +16,8 @@ struct CoreAIFunctionInputsView: View { ) } } + } header: { + Label("Generated Inputs", systemImage: "slider.horizontal.3") } } } diff --git a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionResultsView.swift b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionResultsView.swift index 06d2863..1a0b4b4 100644 --- a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionResultsView.swift +++ b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionResultsView.swift @@ -4,7 +4,7 @@ struct CoreAIFunctionResultsView: View { let result: CoreAIFunctionRunResult var body: some View { - Section("Latest Run") { + Section { LabeledContent("Function", value: result.functionName) LabeledContent("Inference time") { Text(result.duration.formatted(.time(pattern: .minuteSecond))) @@ -12,6 +12,8 @@ struct CoreAIFunctionResultsView: View { ForEach(result.outputs) { output in CoreAIFunctionOutputSummaryView(output: output) } + } header: { + Label("Latest Run", systemImage: "checkmark.circle") } } } diff --git a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionWorkbenchView.swift b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionWorkbenchView.swift index 015e8d5..3bf7062 100644 --- a/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionWorkbenchView.swift +++ b/CoreAILab/Features/FunctionWorkbench/CoreAIFunctionWorkbenchView.swift @@ -37,8 +37,8 @@ struct CoreAIFunctionWorkbenchView: View { Group { if let report = workspace.assetWorkspace.report { - List { - Section("Asset") { + Form { + Section { LabeledContent("Name", value: report.url.lastPathComponent) LabeledContent( "Device", @@ -47,7 +47,11 @@ struct CoreAIFunctionWorkbenchView: View { Text(report.url.path) .font(.callout.monospaced()) .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) .textSelection(.enabled) + } header: { + Label("Asset", systemImage: "shippingbox") } CoreAIRuntimeLifecycleView( @@ -63,32 +67,32 @@ struct CoreAIFunctionWorkbenchView: View { ) if workspace.assetWorkspace.specializationResult == nil { - Section("Function Workbench") { + Section { ContentUnavailableView( "Specialize the Asset", - systemImage: "cpu", - description: Text( - "Choose a compute profile above, then specialize or load its cached model to inspect runtime contracts." - ) + systemImage: "cpu" ) + .help("Choose a compute profile, then specialize or load its cached model.") + } header: { + Label("Function Workbench", systemImage: "function") } } else if workspace.phase == .preparingContracts { - Section("Function Workbench") { + Section { ContentUnavailableView { Label("Reading Function Contracts", systemImage: "list.bullet.rectangle") - } description: { - Text("Loading input, state, and output descriptors from the specialized model.") } actions: { ProgressView() } + } header: { + Label("Function Workbench", systemImage: "function") } } else if workspace.contracts.isEmpty { - Section("Function Workbench") { + Section { ContentUnavailableView { Label( workspace.contractLoadFailureMessage == nil ? "No Functions" - : "Unable to Load Functions", + : "Couldn't Read Functions", systemImage: workspace.contractLoadFailureMessage == nil ? "function" : "exclamationmark.triangle" @@ -107,6 +111,8 @@ struct CoreAIFunctionWorkbenchView: View { ) } } + } header: { + Label("Function Workbench", systemImage: "function") } } else { CoreAIFunctionContractView(workspace: workspace) @@ -115,6 +121,18 @@ struct CoreAIFunctionWorkbenchView: View { isDisabled: workspace.phase.isBusy ) + if workspace.phase == .running { + Section { + ProgressView( + "Running \(workspace.selectedFunctionName ?? "function")…" + ) + .accessibilityAddTraits(.updatesFrequently) + } header: { + Label("Run", systemImage: "play.fill") + } + } + +#if !os(macOS) Section { Button( "Run Function", @@ -123,16 +141,10 @@ struct CoreAIFunctionWorkbenchView: View { ) .buttonStyle(.borderedProminent) .disabled(!workspace.canRun) - - if workspace.phase.isBusy { - Label("Core AI operation in progress", systemImage: "hourglass") - .foregroundStyle(.secondary) - } - } footer: { - Text( - "Generated inputs are synthetic contract probes, not semantically correct task data. Core AI inference itself cannot be canceled once started." - ) + } header: { + Label("Run", systemImage: "play.fill") } +#endif CoreAIFunctionBenchmarkControlsView(workspace: workspace) @@ -153,38 +165,57 @@ struct CoreAIFunctionWorkbenchView: View { ) } } + .formStyle(.grouped) } else if workspace.phase == .loadingAsset || workspace.assetWorkspace.isInspecting { ContentUnavailableView { Label("Opening Model", systemImage: "shippingbox") - } description: { - Text("Inspecting the asset before specialization.") } actions: { ProgressView() } } else { ContentUnavailableView { Label("Function Workbench", systemImage: "function") - } description: { - Text( - "Open a Core AI asset to inspect every function and run supported stateless tensor contracts with generated inputs." - ) } actions: { Button("Open Model", systemImage: "folder", action: openModelPicker) .buttonStyle(.borderedProminent) } + .help("Open a Core AI asset to inspect its functions and supported tensor contracts.") } } .navigationTitle("Function Workbench") .toolbar { - ToolbarItem(placement: .primaryAction) { - Button("Open Model", systemImage: "folder", action: openModelPicker) - .disabled( - workspace.phase.isBusy - || workspace.assetWorkspace.phase.isBusy - || workspace.isExportingIntegration - ) +#if os(macOS) + if workspace.assetWorkspace.report != nil { + ToolbarItemGroup(placement: .primaryAction) { + Button("Open Model", systemImage: "folder", action: openModelPicker) + .disabled( + workspace.phase.isBusy + || workspace.assetWorkspace.phase.isBusy + || workspace.isExportingIntegration + ) + .keyboardShortcut("o", modifiers: .command) + + Button("Run Function", systemImage: "play.fill", action: runFunction) + .disabled(!workspace.canRun) + .help( + "Run synthetic contract inputs. Core AI inference cannot be canceled once started." + ) + } + } +#else + if workspace.assetWorkspace.report != nil { + ToolbarItem(placement: .primaryAction) { + Button("Open Model", systemImage: "folder", action: openModelPicker) + .disabled( + workspace.phase.isBusy + || workspace.assetWorkspace.phase.isBusy + || workspace.isExportingIntegration + ) + .keyboardShortcut("o", modifiers: .command) + } } +#endif } .fileImporter( isPresented: $isImportingModel, @@ -207,11 +238,11 @@ struct CoreAIFunctionWorkbenchView: View { handleBenchmarkEvidenceExport(result) } .alert( - "Function Workbench Error", + "Couldn't Complete the Core AI Operation", isPresented: $assetWorkspace.isShowingError ) { } message: { - Text(assetWorkspace.errorMessage ?? "The Core AI operation failed.") + Text(assetWorkspace.errorMessage ?? "Check the model and configuration, then try again.") } .task(id: initialURL) { do { diff --git a/CoreAILab/Features/FunctionWorkbench/CoreAIIntegrationExportSection.swift b/CoreAILab/Features/FunctionWorkbench/CoreAIIntegrationExportSection.swift index 5c012b5..c8ad514 100644 --- a/CoreAILab/Features/FunctionWorkbench/CoreAIIntegrationExportSection.swift +++ b/CoreAILab/Features/FunctionWorkbench/CoreAIIntegrationExportSection.swift @@ -30,6 +30,9 @@ struct CoreAIIntegrationExportSection: View { action: chooseDestination ) .disabled(!workspace.canExportIntegration) + .help( + "Create a Swift package with the asset, checksums, notices, typed metadata, invocation code, and an offline verifier." + ) } if let status = workspace.exportStatusMessage { @@ -37,11 +40,7 @@ struct CoreAIIntegrationExportSection: View { .foregroundStyle(.secondary) } } header: { - Text("Integration Export") - } footer: { - Text( - "Exports a standalone Swift package with the original asset, checksums, notices, typed metadata, generated invocation code, and an offline verifier. The optional AOT script is never run automatically. Stateful and image-input functions remain manifest-only." - ) + Label("Integration Export", systemImage: "shippingbox.and.arrow.backward") } } diff --git a/CoreAILab/Features/Projects/CoreAIArtifactProjectPickerView.swift b/CoreAILab/Features/Projects/CoreAIArtifactProjectPickerView.swift index ea4ad44..c2a68c5 100644 --- a/CoreAILab/Features/Projects/CoreAIArtifactProjectPickerView.swift +++ b/CoreAILab/Features/Projects/CoreAIArtifactProjectPickerView.swift @@ -18,8 +18,6 @@ struct CoreAIArtifactProjectPickerView: View { if projects.isEmpty { ContentUnavailableView { Label("No Projects", systemImage: "folder.badge.plus") - } description: { - Text("Create a project before storing this conversion output.") } actions: { Button( "New Project", @@ -57,9 +55,9 @@ struct CoreAIArtifactProjectPickerView: View { storeArtifact(in: project) } } - .alert("Artifact Could Not Be Stored", isPresented: $controller.isShowingError) { + .alert("Couldn't Store the Artifact", isPresented: $controller.isShowingError) { } message: { - Text(controller.errorMessage ?? "Core AI Lab could not store the artifact.") + Text(controller.errorMessage ?? "Choose another project or try again.") } } diff --git a/CoreAILab/Features/Projects/CoreAINewProjectView.swift b/CoreAILab/Features/Projects/CoreAINewProjectView.swift index e456a7e..763b8ff 100644 --- a/CoreAILab/Features/Projects/CoreAINewProjectView.swift +++ b/CoreAILab/Features/Projects/CoreAINewProjectView.swift @@ -14,8 +14,13 @@ struct CoreAINewProjectView: View { NavigationStack { Form { - TextField("Project Name", text: $name) - .textContentType(.name) + Section { + TextField("Project Name", text: $name) + .textContentType(.name) + .help("Projects keep related assets, provenance, runs, and evidence together.") + } header: { + Label("Project", systemImage: "folder") + } } .formStyle(.grouped) .navigationTitle("New Project") @@ -27,13 +32,14 @@ struct CoreAINewProjectView: View { ToolbarItem(placement: .confirmationAction) { Button("Create", action: createProject) .disabled(name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + .keyboardShortcut(.defaultAction) } } } - .frame(minWidth: 360, minHeight: 180) - .alert("Project Could Not Be Created", isPresented: $controller.isShowingError) { + .frame(minWidth: 380, minHeight: 220) + .alert("Couldn't Create the Project", isPresented: $controller.isShowingError) { } message: { - Text(controller.errorMessage ?? "Core AI Lab could not create the project.") + Text(controller.errorMessage ?? "Choose a different name and try again.") } } diff --git a/CoreAILab/Features/Projects/CoreAIProjectArtifactDetailView.swift b/CoreAILab/Features/Projects/CoreAIProjectArtifactDetailView.swift index 7b2d982..fbf91b7 100644 --- a/CoreAILab/Features/Projects/CoreAIProjectArtifactDetailView.swift +++ b/CoreAILab/Features/Projects/CoreAIProjectArtifactDetailView.swift @@ -18,7 +18,7 @@ struct CoreAIProjectArtifactDetailView: View { Form { if let artifact = link.artifact { - Section("Artifact") { + Section { LabeledContent("Name", value: link.displayName) LabeledContent( "Kind", @@ -31,9 +31,11 @@ struct CoreAIProjectArtifactDetailView: View { LabeledContent("Imported") { Text(artifact.importedAt, format: .dateTime.day().month().year().hour().minute()) } + } header: { + Label("Artifact", systemImage: "shippingbox") } - Section("Integrity") { + Section { LabeledContent("SHA-256") { Text(artifact.sha256Digest) .font(.callout.monospaced()) @@ -42,8 +44,12 @@ struct CoreAIProjectArtifactDetailView: View { LabeledContent("Store path") { Text(artifact.storageRelativePath) .font(.callout.monospaced()) + .lineLimit(1) + .truncationMode(.middle) .textSelection(.enabled) } + } header: { + Label("Integrity", systemImage: "checkmark.seal") } if artifact.resourceSnapshotData != nil { @@ -69,20 +75,20 @@ struct CoreAIProjectArtifactDetailView: View { "The persisted descriptor snapshot is invalid.", systemImage: "exclamationmark.triangle" ) - .foregroundStyle(.secondary) + .foregroundStyle(.orange) } } if artifact.kind == .modelAsset { - Section("Open With") { - NavigationLink( - "Asset Inspector", - value: CoreAIProjectRoute.inspect(link.id) - ) - NavigationLink( - "Function Workbench", - value: CoreAIProjectRoute.workbench(link.id) - ) + Section { + NavigationLink(value: CoreAIProjectRoute.inspect(link.id)) { + Label("Asset Inspector", systemImage: "doc.text.magnifyingglass") + } + NavigationLink(value: CoreAIProjectRoute.workbench(link.id)) { + Label("Function Workbench", systemImage: "function") + } + } header: { + Label("Open With", systemImage: "arrow.up.forward.app") } CoreAIProjectSpecializationCacheView( @@ -149,9 +155,9 @@ struct CoreAIProjectArtifactDetailView: View { "Project cache records are removed. Core AI deletes configurations only when another project does not still reference them." ) } - .alert("Artifact Operation Failed", isPresented: $controller.isShowingError) { + .alert("Couldn't Update the Artifact", isPresented: $controller.isShowingError) { } message: { - Text(controller.errorMessage ?? "The artifact operation failed.") + Text(controller.errorMessage ?? "Check the stored artifact and try again.") } .sheet(isPresented: $isShowingProvenanceEditor) { CoreAISourceProvenanceEditorView( diff --git a/CoreAILab/Features/Projects/CoreAIProjectArtifactRowView.swift b/CoreAILab/Features/Projects/CoreAIProjectArtifactRowView.swift index 5ffa5ee..824b9e5 100644 --- a/CoreAILab/Features/Projects/CoreAIProjectArtifactRowView.swift +++ b/CoreAILab/Features/Projects/CoreAIProjectArtifactRowView.swift @@ -13,7 +13,7 @@ struct CoreAIProjectArtifactRowView: View { Spacer() if let artifact = link.artifact { - VStack(alignment: .trailing) { + HStack { Text(artifact.byteCount, format: .byteCount(style: .file)) Text(artifact.shortDigest) .monospaced() diff --git a/CoreAILab/Features/Projects/CoreAIProjectDetailView.swift b/CoreAILab/Features/Projects/CoreAIProjectDetailView.swift index 1ec0083..fbd5d6b 100644 --- a/CoreAILab/Features/Projects/CoreAIProjectDetailView.swift +++ b/CoreAILab/Features/Projects/CoreAIProjectDetailView.swift @@ -16,8 +16,8 @@ struct CoreAIProjectDetailView: View { var body: some View { @Bindable var controller = controller - List { - Section("Overview") { + Form { + Section { LabeledContent("Artifacts", value: project.artifactLinks.count.formatted()) LabeledContent("Stored size") { Text(project.storedByteCount, format: .byteCount(style: .file)) @@ -28,16 +28,14 @@ struct CoreAIProjectDetailView: View { LabeledContent("Last opened") { Text(project.lastOpenedAt, format: .relative(presentation: .named)) } + } header: { + Label("Overview", systemImage: "folder") } - Section("Artifacts") { + Section { if project.artifactLinks.isEmpty { ContentUnavailableView { Label("No Stored Artifacts", systemImage: "shippingbox") - } description: { - Text( - "Import a .aimodel package, an Apple resource folder, or a supporting model file." - ) } actions: { Button( "Import Artifact", @@ -54,23 +52,25 @@ struct CoreAIProjectDetailView: View { } if controller.activeProjectID == project.id { - Label( - controller.activeOperation?.title ?? "Updating project…", - systemImage: controller.activeOperation?.systemImage ?? "hourglass" - ) - .foregroundStyle(.secondary) + ProgressView(controller.activeOperation?.title ?? "Updating project…") + .accessibilityAddTraits(.updatesFrequently) } + } header: { + Label("Artifacts", systemImage: "shippingbox") } } + .formStyle(.grouped) .navigationTitle(project.name) .toolbar { ToolbarItemGroup(placement: .primaryAction) { - Button( - "Import Artifact", - systemImage: "square.and.arrow.down", - action: showArtifactImporter - ) - .disabled(controller.isPerformingOperation) + if !project.artifactLinks.isEmpty { + Button( + "Import Artifact", + systemImage: "square.and.arrow.down", + action: showArtifactImporter + ) + .disabled(controller.isPerformingOperation) + } Menu("Project Actions", systemImage: "ellipsis.circle") { Button("Rename Project", systemImage: "pencil", action: showRenamePrompt) @@ -106,9 +106,9 @@ struct CoreAIProjectDetailView: View { "Project metadata is deleted. Stored artifacts are reclaimed only when no other project references the same SHA-256 content." ) } - .alert("Project Operation Failed", isPresented: $controller.isShowingError) { + .alert("Couldn't Update the Project", isPresented: $controller.isShowingError) { } message: { - Text(controller.errorMessage ?? "The project operation failed.") + Text(controller.errorMessage ?? "Check the project and try again.") } .task(id: project.id) { do { @@ -138,7 +138,9 @@ struct CoreAIProjectDetailView: View { } } case .failure(let error): - controller.present(error) + if (error as? CocoaError)?.code != .userCancelled { + controller.present(error) + } } } diff --git a/CoreAILab/Features/Projects/CoreAIProjectLibraryView.swift b/CoreAILab/Features/Projects/CoreAIProjectLibraryView.swift index 9c2b76b..63d3d9a 100644 --- a/CoreAILab/Features/Projects/CoreAIProjectLibraryView.swift +++ b/CoreAILab/Features/Projects/CoreAIProjectLibraryView.swift @@ -8,17 +8,18 @@ struct CoreAIProjectLibraryView: View { @State private var controller = CoreAIProjectLibraryController() @State private var path: [CoreAIProjectRoute] = [] @State private var isCreatingProject = false + @State private var searchText = "" var body: some View { + let visibleProjects = searchText.isEmpty + ? projects + : projects.filter { $0.name.localizedStandardContains(searchText) } + NavigationStack(path: $path) { Group { if projects.isEmpty { ContentUnavailableView { - Label("Create a Lab Project", systemImage: "folder.badge.plus") - } description: { - Text( - "Projects keep imported models and resource bundles available across launches with checksummed, deduplicated storage." - ) + Label("Create Your First Project", systemImage: "folder.badge.plus") } actions: { Button( "New Project", @@ -28,23 +29,30 @@ struct CoreAIProjectLibraryView: View { .buttonStyle(.borderedProminent) } } else { - List(projects) { project in - NavigationLink(value: CoreAIProjectRoute.project(project.id)) { - CoreAIProjectRowView(project: project) + Group { + if visibleProjects.isEmpty { + ContentUnavailableView.search + } else { + List(visibleProjects) { project in + NavigationLink(value: CoreAIProjectRoute.project(project.id)) { + CoreAIProjectRowView(project: project) + } + } + } + } + .searchable(text: $searchText, prompt: "Search projects") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button( + "New Project", + systemImage: "plus", + action: showNewProject + ) } } } } .navigationTitle("Projects") - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button( - "New Project", - systemImage: "plus", - action: showNewProject - ) - } - } .navigationDestination(for: CoreAIProjectRoute.self) { route in CoreAIProjectDestinationView( route: route, @@ -58,9 +66,9 @@ struct CoreAIProjectLibraryView: View { path.append(.project(project.id)) } } - .alert("Project Operation Failed", isPresented: $controller.isShowingError) { + .alert("Couldn't Update the Project Library", isPresented: $controller.isShowingError) { } message: { - Text(controller.errorMessage ?? "The project operation failed.") + Text(controller.errorMessage ?? "Check project storage and try again.") } } diff --git a/CoreAILab/Features/Projects/CoreAIProjectRowView.swift b/CoreAILab/Features/Projects/CoreAIProjectRowView.swift index b8d90b3..fdde7c3 100644 --- a/CoreAILab/Features/Projects/CoreAIProjectRowView.swift +++ b/CoreAILab/Features/Projects/CoreAIProjectRowView.swift @@ -4,25 +4,21 @@ struct CoreAIProjectRowView: View { let project: LabProject var body: some View { - HStack { - Label(project.name, systemImage: "folder") + HStack(alignment: .firstTextBaseline) { + Label(project.name, systemImage: "folder.fill") + .font(.headline) Spacer() - VStack(alignment: .trailing) { - Text(project.artifactLinks.count, format: .number) - .monospacedDigit() - Text( - project.storedByteCount, - format: .byteCount(style: .file) - ) - } - .font(.callout) - .foregroundStyle(.secondary) + Text("^[\(project.artifactLinks.count) artifact](inflect: true)") + .foregroundStyle(.secondary) } .accessibilityElement(children: .combine) .accessibilityLabel( "\(project.name), \(project.artifactLinks.count) artifacts, \(project.storedByteCount.formatted(.byteCount(style: .file)))" ) + .help( + "\(project.storedByteCount.formatted(.byteCount(style: .file))) · Updated \(project.updatedAt.formatted(.relative(presentation: .named)))" + ) } } diff --git a/CoreAILab/Features/Projects/CoreAIProjectSpecializationCacheView.swift b/CoreAILab/Features/Projects/CoreAIProjectSpecializationCacheView.swift index 6ee7c93..4319201 100644 --- a/CoreAILab/Features/Projects/CoreAIProjectSpecializationCacheView.swift +++ b/CoreAILab/Features/Projects/CoreAIProjectSpecializationCacheView.swift @@ -8,10 +8,9 @@ struct CoreAIProjectSpecializationCacheView: View { let removeAll: () -> Void var body: some View { - Section("Specialization Cache") { + Section { if link.specializationCaches.isEmpty { - Text("Specialize this project artifact to register a cache entry.") - .foregroundStyle(.secondary) + Label("No Cached Configurations", systemImage: "minus.circle") } else { ForEach(link.sortedSpecializationCaches) { record in CoreAISpecializationCacheRowView( @@ -31,9 +30,11 @@ struct CoreAIProjectSpecializationCacheView: View { } if isUpdatingCache { - Label("Updating Core AI cache…", systemImage: "hourglass") - .foregroundStyle(.secondary) + ProgressView("Updating Core AI cache…") + .accessibilityAddTraits(.updatesFrequently) } + } header: { + Label("Specialization Cache", systemImage: "cpu") } } } diff --git a/CoreAILab/Features/Projects/CoreAISourceProvenanceEditorView.swift b/CoreAILab/Features/Projects/CoreAISourceProvenanceEditorView.swift index a0170d4..2f88b11 100644 --- a/CoreAILab/Features/Projects/CoreAISourceProvenanceEditorView.swift +++ b/CoreAILab/Features/Projects/CoreAISourceProvenanceEditorView.swift @@ -32,36 +32,42 @@ struct CoreAISourceProvenanceEditorView: View { var body: some View { NavigationStack { Form { - Picker("Source type", selection: $kind) { - ForEach(CoreAISourceProvenanceKind.allCases) { kind in - Text(kind.title).tag(kind) + Section { + Picker("Source type", selection: $kind) { + ForEach(CoreAISourceProvenanceKind.allCases) { kind in + Text(kind.title).tag(kind) + } } - } - TextField("Source location", text: $sourceLocation, axis: .vertical) - .lineLimit(2...5) - TextField("Provider", text: $providerName) - TextField("License", text: $licenseName) - TextField("Notes", text: $notes, axis: .vertical) - .lineLimit(3...8) + TextField("Source location", text: $sourceLocation, axis: .vertical) + .lineLimit(2...5) + .help("Record enough detail to trace the artifact to its source and license.") + TextField("Provider", text: $providerName) + TextField("License", text: $licenseName) + TextField("Notes", text: $notes, axis: .vertical) + .lineLimit(3...8) + } header: { + Label("Source", systemImage: "link") + } } .formStyle(.grouped) .navigationTitle("Source Provenance") .toolbar { ToolbarItem(placement: .cancellationAction) { - Button("Cancel", action: dismiss.callAsFunction) + Button("Cancel", role: .cancel, action: dismiss.callAsFunction) } ToolbarItem(placement: .confirmationAction) { Button("Save", action: save) .disabled(kind != .unknown && sourceLocation.trimmed.isEmpty) + .keyboardShortcut(.defaultAction) } } - .alert("Unable to Save Provenance", isPresented: $isShowingError) { - Button("OK") { + .alert("Couldn't Save Provenance", isPresented: $isShowingError) { + Button("Dismiss", role: .cancel) { errorMessage = nil } } message: { - Text(errorMessage ?? "The source provenance could not be saved.") + Text(errorMessage ?? "Check the source details and try again.") } } } diff --git a/CoreAILab/Features/Projects/CoreAISourceProvenanceSummaryView.swift b/CoreAILab/Features/Projects/CoreAISourceProvenanceSummaryView.swift index 5ac2f75..2f8029e 100644 --- a/CoreAILab/Features/Projects/CoreAISourceProvenanceSummaryView.swift +++ b/CoreAILab/Features/Projects/CoreAISourceProvenanceSummaryView.swift @@ -24,12 +24,10 @@ struct CoreAISourceProvenanceSummaryView: View { LabeledContent("License", value: provenance.licenseName) } if !provenance.notes.isEmpty { - Text(provenance.notes) - .foregroundStyle(.secondary) + LabeledContent("Notes", value: provenance.notes) } } else { - Text("No source provenance has been recorded.") - .foregroundStyle(.secondary) + Label("No Source Provenance", systemImage: "minus.circle") } Button( "Edit Source Provenance", diff --git a/CoreAILab/Features/Projects/CoreAISpecializationCacheRowView.swift b/CoreAILab/Features/Projects/CoreAISpecializationCacheRowView.swift index 7ef9065..4deea87 100644 --- a/CoreAILab/Features/Projects/CoreAISpecializationCacheRowView.swift +++ b/CoreAILab/Features/Projects/CoreAISpecializationCacheRowView.swift @@ -7,23 +7,16 @@ struct CoreAISpecializationCacheRowView: View { var body: some View { HStack { - VStack(alignment: .leading) { - Label(record.configurationTitle, systemImage: "cpu") - Text( - record.lastUsedAt, - format: .relative(presentation: .named) - ) - .foregroundStyle(.secondary) - Text( - record.wasLoadedFromCache - ? "Loaded from existing cache" - : "Created by specialization" - ) - .foregroundStyle(.secondary) - } + Label(record.configurationTitle, systemImage: "cpu") Spacer() + Text( + record.lastUsedAt, + format: .relative(presentation: .named) + ) + .foregroundStyle(.secondary) + Button( "Remove \(record.configurationTitle)", systemImage: "trash", @@ -35,5 +28,10 @@ struct CoreAISpecializationCacheRowView: View { .disabled(isDisabled) } .accessibilityElement(children: .contain) + .help( + record.wasLoadedFromCache + ? "Loaded from an existing specialization cache" + : "Created by specialization" + ) } } diff --git a/CoreAILab/Features/RecipeStudio/CoreAIPipelineStudioView.swift b/CoreAILab/Features/RecipeStudio/CoreAIPipelineStudioView.swift index a8b0eef..9bdece5 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIPipelineStudioView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIPipelineStudioView.swift @@ -63,16 +63,14 @@ struct CoreAIPipelineStudioView: View { action: workspace.connectSelectedEndpoints ) .disabled(!workspace.canConnectSelectedEndpoints) + .help("Each destination accepts one compatible source value contract.") } header: { Text("Connect Typed Ports") - } footer: { - Text("Only compatible value contracts can be connected, and each destination accepts one source.") } Section("Edges") { if displayedEdges.isEmpty { - Text("No edges") - .foregroundStyle(.secondary) + ContentUnavailableView("No Edges", systemImage: "arrow.triangle.branch") } ForEach(displayedEdges) { edge in LabeledContent( diff --git a/CoreAILab/Features/RecipeStudio/CoreAIPipelineValidationIssuesView.swift b/CoreAILab/Features/RecipeStudio/CoreAIPipelineValidationIssuesView.swift index 47572b3..3664485 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIPipelineValidationIssuesView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIPipelineValidationIssuesView.swift @@ -5,7 +5,7 @@ struct CoreAIPipelineValidationIssuesView: View { var body: some View { if issues.isEmpty { - Label("Pipeline contract is valid", systemImage: "checkmark.circle.fill") + Label("Contract Valid", systemImage: "checkmark.circle.fill") .foregroundStyle(.green) } else { ForEach(issues) { issue in diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeDynamicDimensionsEditorView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeDynamicDimensionsEditorView.swift index 2fb9fdc..3ab6f98 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeDynamicDimensionsEditorView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeDynamicDimensionsEditorView.swift @@ -8,9 +8,9 @@ struct CoreAIRecipeDynamicDimensionsEditorView: View { if workspace.recipe.dynamicDimensions.isEmpty { ContentUnavailableView( "No Dynamic Dimensions", - systemImage: "arrow.left.and.right", - description: Text("Static example shapes remain unchanged until a bounded dynamic axis is added.") + systemImage: "arrow.left.and.right" ) + .help("Static example shapes remain unchanged until a bounded dynamic axis is added.") } ForEach($workspace.recipe.dynamicDimensions) { $dimension in @@ -45,8 +45,9 @@ struct CoreAIRecipeDynamicDimensionsEditorView: View { action: workspace.addDynamicDimension ) .disabled(!workspace.canAddDynamicDimension) - } footer: { - Text("Bounds are authoring constraints, not evidence that every shape specializes or runs on a preferred compute unit.") + .help( + "Bounds are authoring constraints; they do not prove specialization or execution placement." + ) } } .formStyle(.grouped) diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeExampleInputsEditorView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeExampleInputsEditorView.swift index 7f127d3..0f0e980 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeExampleInputsEditorView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeExampleInputsEditorView.swift @@ -8,9 +8,9 @@ struct CoreAIRecipeExampleInputsEditorView: View { if workspace.recipe.exampleInputs.isEmpty { ContentUnavailableView( "No Example Inputs", - systemImage: "square.and.pencil", - description: Text("Add the concrete arguments used to export and validate this recipe.") + systemImage: "square.and.pencil" ) + .help("Add the concrete arguments used to export and validate this recipe.") } ForEach($workspace.recipe.exampleInputs) { $input in diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeExternalizationEditorView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeExternalizationEditorView.swift index 5b76e87..bda4766 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeExternalizationEditorView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeExternalizationEditorView.swift @@ -8,9 +8,9 @@ struct CoreAIRecipeExternalizationEditorView: View { if workspace.recipe.externalizationRules.isEmpty { ContentUnavailableView( "No Externalization Rules", - systemImage: "externaldrive", - description: Text("Weights remain under the converter's default policy until a module rule is added.") + systemImage: "externaldrive" ) + .help("Weights use the converter's default policy until a module rule is added.") } ForEach($workspace.recipe.externalizationRules) { $rule in diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeFunctionEntrypointsEditorView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeFunctionEntrypointsEditorView.swift index d3c9e4f..d05045d 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeFunctionEntrypointsEditorView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeFunctionEntrypointsEditorView.swift @@ -8,9 +8,9 @@ struct CoreAIRecipeFunctionEntrypointsEditorView: View { if workspace.recipe.functionEntrypoints.isEmpty { ContentUnavailableView( "No Function Entrypoints", - systemImage: "function", - description: Text("Define at least one exported module method and its named contract.") + systemImage: "function" ) + .help("Define at least one exported module method and its named contract.") } ForEach($workspace.recipe.functionEntrypoints) { $function in diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeGeneratedArtifactsView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeGeneratedArtifactsView.swift index 25a4c6e..0a6f869 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeGeneratedArtifactsView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeGeneratedArtifactsView.swift @@ -8,9 +8,9 @@ struct CoreAIRecipeGeneratedArtifactsView: View { if workspace.generatedArtifacts.isEmpty { ContentUnavailableView( "No Generated Stubs", - systemImage: "doc.badge.gearshape", - description: Text("Generate stubs from an attributed unsupported-operation finding.") + systemImage: "doc.badge.gearshape" ) + .help("Generate stubs from an attributed unsupported-operation finding.") } ForEach(workspace.generatedArtifacts) { artifact in diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeReferenceListEditorView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeReferenceListEditorView.swift index 8699b1d..51f00ec 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeReferenceListEditorView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeReferenceListEditorView.swift @@ -22,10 +22,6 @@ struct CoreAIRecipeReferenceListEditorView: View { GroupBox(title) { VStack(alignment: .leading) { - if valueIDs.isEmpty { - Text("None") - .foregroundStyle(.secondary) - } ForEach(valueIDs, id: \.self) { valueID in if let value = value(for: valueID) { HStack { diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeRewriteCatalogView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeRewriteCatalogView.swift index e15062b..f502745 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeRewriteCatalogView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeRewriteCatalogView.swift @@ -3,20 +3,19 @@ import SwiftUI struct CoreAIRecipeRewriteCatalogView: View { var body: some View { List(CoreAIRecipeRewriteCatalog.builtIn) { rewrite in - VStack(alignment: .leading) { + HStack(alignment: .firstTextBaseline) { Label(rewrite.title, systemImage: "arrow.trianglehead.2.clockwise.rotate.90") .font(.headline) + + Spacer() + Text(rewrite.strategy.title) - .font(.subheadline) - .foregroundStyle(.secondary) - Text(rewrite.summary) - Text(rewrite.operatorNames.joined(separator: ", ")) - .font(.body.monospaced()) - .textSelection(.enabled) - Text(rewrite.evidence) - .font(.footnote) + .font(.callout) .foregroundStyle(.secondary) } + .help( + "\(rewrite.summary) Operators: \(rewrite.operatorNames.joined(separator: ", ")). \(rewrite.evidence)" + ) } .navigationTitle("Rewrite Catalog") } diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeSourceEditorView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeSourceEditorView.swift index 533b611..c35dd25 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeSourceEditorView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeSourceEditorView.swift @@ -5,10 +5,12 @@ struct CoreAIRecipeSourceEditorView: View { var body: some View { Form { - Section("Recipe") { + Section { TextField("Display name", text: $workspace.recipe.displayName) TextField("Recipe ID", text: $workspace.recipe.id) .coreAIRecipeIdentifierInput() + } header: { + Label("Recipe", systemImage: "doc.text") } Section { @@ -21,13 +23,12 @@ struct CoreAIRecipeSourceEditorView: View { .coreAIRecipeIdentifierInput() TextField("Pinned revision", text: $workspace.recipe.source.revision) .coreAIRecipeIdentifierInput() + .help("Pin a revision before executing a reproducible conversion.") } header: { - Text("PyTorch Source") - } footer: { - Text("A blank revision is allowed while drafting, but a reproducible conversion should pin one before execution.") + Label("PyTorch Source", systemImage: "shippingbox") } - Section("Module") { + Section { TextField("Python module path", text: $workspace.recipe.module.modulePath) .coreAIRecipeIdentifierInput() TextField("Module type", text: $workspace.recipe.module.typeName) @@ -36,10 +37,14 @@ struct CoreAIRecipeSourceEditorView: View { .coreAIRecipeIdentifierInput() TextField("Checkpoint path", text: $workspace.recipe.module.checkpointPath) .coreAIRecipeIdentifierInput() + } header: { + Label("Module", systemImage: "cube") } - Section("Validation") { + Section { CoreAIRecipeValidationIssuesView(issues: workspace.validationIssues) + } header: { + Label("Validation", systemImage: "checkmark.shield") } } .formStyle(.grouped) diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeStateEditorView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeStateEditorView.swift index 8072303..db8788c 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeStateEditorView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeStateEditorView.swift @@ -8,9 +8,9 @@ struct CoreAIRecipeStateEditorView: View { if workspace.recipe.stateBindings.isEmpty { ContentUnavailableView( "No Explicit State", - systemImage: "memorychip", - description: Text("Add state only when the exported functions expose named input and output bindings.") + systemImage: "memorychip" ) + .help("Add state only when exported functions expose named input and output bindings.") } ForEach($workspace.recipe.stateBindings) { $state in diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioPanel.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioPanel.swift index 9013e51..1a3de36 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioPanel.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioPanel.swift @@ -63,6 +63,31 @@ enum CoreAIRecipeStudioPanel: String, CaseIterable, Hashable, Identifiable { "point.3.connected.trianglepath.dotted" } } + + var summary: String { + switch self { + case .source: + "Define the recipe identity, source revision, and Python module." + case .exampleInputs: + "Describe deterministic inputs for conversion and validation." + case .dynamicDimensions: + "Name the dimensions that may vary at runtime." + case .state: + "Describe mutable tensors carried between function calls." + case .externalization: + "Choose resources that live outside the compiled model asset." + case .functions: + "Define callable entry points and their typed inputs and outputs." + case .diagnostics: + "Review source operators the Core AI converter cannot lower." + case .rewrites: + "Inspect the built-in catalog of supported graph rewrites." + case .generatedArtifacts: + "Review generated stubs without executing authored code." + case .pipeline: + "Connect typed assets into a validated pipeline contract." + } + } } extension View { diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioPanelLink.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioPanelLink.swift index 4b9d928..97043a9 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioPanelLink.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioPanelLink.swift @@ -7,5 +7,7 @@ struct CoreAIRecipeStudioPanelLink: View { NavigationLink(value: panel) { Label(panel.title, systemImage: panel.systemImage) } + .help(panel.summary) + .accessibilityHint(panel.summary) } } diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioView.swift index 0ec0ab4..f2630d4 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeStudioView.swift @@ -2,7 +2,8 @@ import SwiftUI struct CoreAIRecipeStudioView: View { @State private var workspace: CoreAIRecipeStudioWorkspaceModel - @State private var selection: CoreAIRecipeStudioPanel? = .source + @SceneStorage("CoreAILab.recipeStudio.selectedPanel") + private var selectedPanelRawValue = CoreAIRecipeStudioPanel.source.rawValue init(recipe: CoreAIRecipeAuthoringManifest = .starter) { _workspace = State(initialValue: CoreAIRecipeStudioWorkspaceModel(recipe: recipe)) @@ -10,7 +11,13 @@ struct CoreAIRecipeStudioView: View { var body: some View { NavigationSplitView { - List(selection: $selection) { + List(selection: selectedPanelBinding) { + Section { + Text(workspace.recipe.displayName) + .font(.headline) + .lineLimit(2) + } + Section("Authoring") { CoreAIRecipeStudioPanelLink(panel: .source) CoreAIRecipeStudioPanelLink(panel: .exampleInputs) @@ -31,14 +38,15 @@ struct CoreAIRecipeStudioView: View { } Section("Draft Status") { - LabeledContent("Validation issues") { - Text(workspace.validationIssues.count, format: .number) - } + Label(validationTitle, systemImage: validationSystemImage) + .foregroundStyle(validationStyle) } } + .listStyle(.sidebar) .navigationTitle("Recipe Studio") + .navigationSplitViewColumnWidth(min: 190, ideal: 220, max: 260) } detail: { - switch selection ?? .source { + switch selectedPanel { case .source: CoreAIRecipeSourceEditorView(workspace: workspace) case .exampleInputs: @@ -63,6 +71,40 @@ struct CoreAIRecipeStudioView: View { } .navigationSplitViewStyle(.balanced) } + + private var selectedPanel: CoreAIRecipeStudioPanel { + CoreAIRecipeStudioPanel(rawValue: selectedPanelRawValue) ?? .source + } + + private var selectedPanelBinding: Binding { + Binding( + get: { selectedPanel }, + set: { selectedPanelRawValue = ($0 ?? .source).rawValue } + ) + } + + private var validationTitle: String { + let count = workspace.validationIssues.count + if count == 0 { + return "Structurally valid" + } else if count == 1 { + return "1 validation issue" + } else { + return "\(count) validation issues" + } + } + + private var validationSystemImage: String { + workspace.validationIssues.isEmpty + ? "checkmark.circle.fill" + : "exclamationmark.triangle.fill" + } + + private var validationStyle: AnyShapeStyle { + workspace.validationIssues.isEmpty + ? AnyShapeStyle(.green) + : AnyShapeStyle(.orange) + } } #Preview { diff --git a/CoreAILab/Features/RecipeStudio/CoreAIRecipeValidationIssuesView.swift b/CoreAILab/Features/RecipeStudio/CoreAIRecipeValidationIssuesView.swift index 962ac68..33cad95 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIRecipeValidationIssuesView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIRecipeValidationIssuesView.swift @@ -5,7 +5,7 @@ struct CoreAIRecipeValidationIssuesView: View { var body: some View { if issues.isEmpty { - Label("This draft is structurally valid", systemImage: "checkmark.circle.fill") + Label("Structurally Valid", systemImage: "checkmark.circle.fill") .foregroundStyle(.green) } else { ForEach(issues) { issue in diff --git a/CoreAILab/Features/RecipeStudio/CoreAIUnsupportedOperationReportView.swift b/CoreAILab/Features/RecipeStudio/CoreAIUnsupportedOperationReportView.swift index 055b2c7..3c3a5ed 100644 --- a/CoreAILab/Features/RecipeStudio/CoreAIUnsupportedOperationReportView.swift +++ b/CoreAILab/Features/RecipeStudio/CoreAIUnsupportedOperationReportView.swift @@ -8,9 +8,9 @@ struct CoreAIUnsupportedOperationReportView: View { if workspace.recipe.unsupportedOperations.isEmpty { ContentUnavailableView( "No Unsupported Operations Reported", - systemImage: "checkmark.circle", - description: Text("This means the draft has no imported findings; it does not prove that export will succeed.") + systemImage: "checkmark.circle" ) + .help("No imported findings does not prove that export will succeed.") } ForEach(workspace.recipe.unsupportedOperations) { finding in diff --git a/CoreAILab/Features/Recipes/CoreAIImportedRecipeBundleView.swift b/CoreAILab/Features/Recipes/CoreAIImportedRecipeBundleView.swift index 9c098fe..4f125b7 100644 --- a/CoreAILab/Features/Recipes/CoreAIImportedRecipeBundleView.swift +++ b/CoreAILab/Features/Recipes/CoreAIImportedRecipeBundleView.swift @@ -10,27 +10,28 @@ struct CoreAIImportedRecipeBundleView: View { var body: some View { if let summary { - VStack(alignment: .leading, spacing: 8) { - Text(summary.manifest.displayName) + VStack(alignment: .leading) { + Label(summary.manifest.displayName, systemImage: "shippingbox.fill") .font(.headline) LabeledContent("Trust", value: summary.trustState.displayName) LabeledContent( "Bundle SHA-256", - value: String(summary.manifestSHA256.prefix(12)) + value: summary.manifestSHA256 ) .font(.callout.monospaced()) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) LabeledContent( "Code references", value: "\(summary.manifest.codeReferences.count)" ) ForEach(summary.manifest.codeReferences) { reference in - VStack(alignment: .leading, spacing: 2) { - Text(reference.id) - .font(.callout.weight(.medium)) - Text("\(reference.language.rawValue) · \(reference.relativePath) · \(reference.entryPoint)") - .font(.caption.monospaced()) - .foregroundStyle(.secondary) - } + LabeledContent( + reference.id, + value: "\(reference.language.rawValue.capitalized) · \(reference.entryPoint)" + ) + .help(reference.relativePath) } } @@ -42,9 +43,9 @@ struct CoreAIImportedRecipeBundleView: View { } else { ContentUnavailableView( "No Imported Bundle", - systemImage: "shippingbox", - description: Text(statusMessage) + systemImage: "shippingbox" ) + .help(statusMessage) } if isImporting { @@ -52,10 +53,6 @@ struct CoreAIImportedRecipeBundleView: View { ProgressView() Text(statusMessage) } - } else if summary != nil { - Text(statusMessage) - .font(.caption) - .foregroundStyle(.secondary) } } diff --git a/CoreAILab/Features/Recipes/CoreAIRecipeCatalogEntryView.swift b/CoreAILab/Features/Recipes/CoreAIRecipeCatalogEntryView.swift index 96490d9..3f0a9fe 100644 --- a/CoreAILab/Features/Recipes/CoreAIRecipeCatalogEntryView.swift +++ b/CoreAILab/Features/Recipes/CoreAIRecipeCatalogEntryView.swift @@ -4,36 +4,72 @@ struct CoreAIRecipeCatalogEntryView: View { let entry: CoreAIRecipeCatalogEntry var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text(entry.displayName) + VStack(alignment: .leading) { + Label(entry.displayName, systemImage: "waveform.badge.microphone") .font(.headline) - Text(entry.summary) - .foregroundStyle(.secondary) - LabeledContent("Trust", value: entry.trustState.displayName) - LabeledContent( - "Verification", - value: entry.verificationState.displayName - ) - LabeledContent( - "Recipe SHA-256", - value: entry.recipeManifestSHA256 - ) - .font(.callout.monospaced()) - .textSelection(.enabled) - Text(entry.verificationNotes) - .font(.caption) - .foregroundStyle(.secondary) + + Divider() + + LabeledContent("Trust") { + Label(entry.trustState.displayName, systemImage: trustSystemImage) + } + LabeledContent("Verification") { + Label( + entry.verificationState.displayName, + systemImage: verificationSystemImage + ) + } + LabeledContent("Recipe SHA-256") { + Text(entry.recipeManifestSHA256) + .font(.callout.monospaced()) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } + if let evidenceReference = entry.evidenceReference { - Text("Evidence: \(evidenceReference)") - .font(.caption.monospaced()) - .foregroundStyle(.secondary) + LabeledContent("Evidence") { + Text(evidenceReference) + .font(.callout.monospaced()) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } } if let evidenceSHA256 = entry.evidenceSHA256 { - LabeledContent("Evidence SHA-256", value: evidenceSHA256) - .font(.caption.monospaced()) - .textSelection(.enabled) + LabeledContent("Evidence SHA-256") { + Text(evidenceSHA256) + .font(.callout.monospaced()) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } } } - .padding(.vertical, 4) + .help("\(entry.summary) \(entry.verificationNotes)") + } + + private var trustSystemImage: String { + switch entry.trustState { + case .bundledCurated: + "checkmark.seal.fill" + case .publisherReviewed: + "person.badge.shield.checkmark" + case .importedUntrusted: + "exclamationmark.shield" + } + } + + private var verificationSystemImage: String { + switch entry.verificationState { + case .notVerified: + "questionmark.diamond" + case .schemaValidated: + "doc.badge.checkmark" + case .fixturesValidated: + "checkmark.rectangle.stack" + case .hardwareValidated: + "checkmark.circle.fill" + } } } diff --git a/CoreAILab/Features/Recipes/CoreAIRecipeCatalogView.swift b/CoreAILab/Features/Recipes/CoreAIRecipeCatalogView.swift index cdaae67..cfe13dd 100644 --- a/CoreAILab/Features/Recipes/CoreAIRecipeCatalogView.swift +++ b/CoreAILab/Features/Recipes/CoreAIRecipeCatalogView.swift @@ -10,13 +10,8 @@ struct CoreAIRecipeCatalogView: View { let entries = model.entries NavigationStack { - List { + Form { Section { - Text("Trust describes where a recipe came from. Verification describes which checks have evidence. Neither state grants imported code permission to run.") - .foregroundStyle(.secondary) - } - - Section("Curated recipes") { if let catalogError = model.catalogError { ContentUnavailableView( "Catalog Unavailable", @@ -33,9 +28,14 @@ struct CoreAIRecipeCatalogView: View { CoreAIRecipeCatalogEntryView(entry: entry) } } + } header: { + Label("Curated Recipes", systemImage: "checkmark.seal") } + .help( + "Trust identifies source. Verification names evidence-backed checks; neither grants code permission." + ) - Section("Imported bundle") { + Section { CoreAIImportedRecipeBundleView( summary: model.importedSummary, codeApprovalState: model.codeApprovalState, @@ -44,8 +44,11 @@ struct CoreAIRecipeCatalogView: View { onApprove: approveReferencedCode, onRevoke: revokeReferencedCode ) + } header: { + Label("Imported Bundle", systemImage: "shippingbox.and.arrow.backward") } } + .formStyle(.grouped) .navigationTitle("Recipes") .toolbar { ToolbarItem(placement: .primaryAction) { @@ -65,7 +68,7 @@ struct CoreAIRecipeCatalogView: View { handleImportResult(result) } .alert( - "Recipe Bundle Import Failed", + "Couldn't Import the Recipe Bundle", isPresented: $model.isShowingError, presenting: model.errorMessage ) { _ in @@ -85,7 +88,9 @@ struct CoreAIRecipeCatalogView: View { guard let url = urls.first else { return } Task { await model.importBundle(at: url) } case .failure(let error): - model.presentImportError(error) + if (error as? CocoaError)?.code != .userCancelled { + model.presentImportError(error) + } } } diff --git a/CoreAILab/Features/Recipes/CoreAIRecipeCatalogWorkspaceModel.swift b/CoreAILab/Features/Recipes/CoreAIRecipeCatalogWorkspaceModel.swift index 7c02a9b..930a2fa 100644 --- a/CoreAILab/Features/Recipes/CoreAIRecipeCatalogWorkspaceModel.swift +++ b/CoreAILab/Features/Recipes/CoreAIRecipeCatalogWorkspaceModel.swift @@ -80,7 +80,7 @@ final class CoreAIRecipeCatalogWorkspaceModel { } catch is CancellationError { guard activeImportID == importID else { return } phase = .idle - statusMessage = "Import cancelled." + statusMessage = "Import canceled." } catch { guard activeImportID == importID else { return } phase = .idle diff --git a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceRoute.swift b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceRoute.swift index 44044f0..9bb9064 100644 --- a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceRoute.swift +++ b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceRoute.swift @@ -4,6 +4,6 @@ struct CoreAIRuntimeExperienceRoute: Hashable { let experienceID: String var unavailableDescription: String { - "The experience “\(experienceID)” is not available in the current runtime registry or on this platform." + "The experience “\(experienceID)” is no longer in the runtime registry or isn't available on this platform." } } diff --git a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceRow.swift b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceRow.swift index 4564745..132ec0a 100644 --- a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceRow.swift +++ b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceRow.swift @@ -4,33 +4,38 @@ struct CoreAIRuntimeExperienceRow: View { let mapping: CoreAIRecipeExperienceMapping var body: some View { - VStack(alignment: .leading) { + HStack(alignment: .firstTextBaseline) { Label( mapping.experience.title, systemImage: mapping.experience.systemImage ) .font(.headline) - Text(mapping.experience.summary) - .foregroundStyle(.secondary) - - Text(capabilitySummary) - .font(.callout) - .foregroundStyle(.secondary) + Spacer() - Label(platformSummary, systemImage: "desktopcomputer.and.iphone") + Label(platformSummary, systemImage: platformSystemImage) .font(.callout) .foregroundStyle(.secondary) } + .padding(.vertical, 4) .accessibilityElement(children: .combine) .accessibilityHint("Opens the local runtime experience") + .help("\(mapping.experience.summary) \(capabilitySummary)") } private var capabilitySummary: String { - mapping.experience.capabilities.map(\.title).joined(separator: ", ") + mapping.experience.capabilities.map(\.title).joined(separator: " · ") } private var platformSummary: String { mapping.experience.platforms.map(\.rawValue).joined(separator: " · ") } + + private var platformSystemImage: String { + mapping.experience.platforms.count > 1 + ? "desktopcomputer.and.iphone" + : mapping.experience.platforms.first == .iOS + ? "iphone" + : "desktopcomputer" + } } diff --git a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceSectionView.swift b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceSectionView.swift index e436e21..ce471d5 100644 --- a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceSectionView.swift +++ b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeExperienceSectionView.swift @@ -5,7 +5,7 @@ struct CoreAIRuntimeExperienceSectionView: View { let mappings: [CoreAIRecipeExperienceMapping] var body: some View { - Section(workload.title) { + Section { ForEach(mappings) { mapping in NavigationLink( value: CoreAIRuntimeExperienceRoute( @@ -15,6 +15,27 @@ struct CoreAIRuntimeExperienceSectionView: View { CoreAIRuntimeExperienceRow(mapping: mapping) } } + } header: { + Label(workload.title, systemImage: workloadSystemImage) + } + } + + private var workloadSystemImage: String { + switch workload { + case .audioTranscription: + "waveform" + case .embedding: + "point.3.connected.trianglepath.dotted" + case .genericFunction: + "function" + case .imageGeneration: + "photo" + case .objectDetection: + "viewfinder" + case .segmentation: + "square.3.layers.3d" + case .textGeneration: + "text.bubble" } } } diff --git a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeLifecycleView.swift b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeLifecycleView.swift index 4b1eb11..5e3696f 100644 --- a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeLifecycleView.swift +++ b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeLifecycleView.swift @@ -5,40 +5,31 @@ struct CoreAIRuntimeLifecycleView: View { let context: CoreAIRuntimeRunContext var body: some View { - GroupBox("Shared Run Lifecycle") { - if let run = coordinator.latestRun(for: context.experienceID) { - VStack(alignment: .leading) { + if let run = coordinator.latestRun(for: context.experienceID) { + Section { + LabeledContent("Status") { Label(run.state.title, systemImage: run.state.systemImage) - LabeledContent("Timing class", value: run.timingClass.title) - LabeledContent("Model identity", value: run.modelIdentity) - if let durationSeconds = run.durationSeconds { - LabeledContent("Elapsed") { - Text( - durationSeconds, - format: .number.precision(.fractionLength(3)) - ) - Text("seconds") - } - } - if let comparison = run.selectedComparisonIdentity { - LabeledContent( - "Comparison identity", - value: comparison.displayName + } + LabeledContent("Timing class", value: run.timingClass.title) + LabeledContent("Model identity", value: run.modelIdentity) + if let durationSeconds = run.durationSeconds { + LabeledContent("Elapsed") { + Text( + durationSeconds, + format: .number.precision(.fractionLength(3)) ) + Text("seconds") } - Text(run.summary) - .foregroundStyle(.secondary) } - .frame(maxWidth: .infinity, alignment: .leading) - .accessibilityElement(children: .contain) - } else { - ContentUnavailableView( - "No Runtime Run Yet", - systemImage: "clock", - description: Text( - "Import the required model and inputs, then run this experience." + if let comparison = run.selectedComparisonIdentity { + LabeledContent( + "Comparison identity", + value: comparison.displayName ) - ) + } + LabeledContent("Summary", value: run.summary) + } header: { + Label("Run Record", systemImage: "clock") } } } diff --git a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeRecentRunsView.swift b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeRecentRunsView.swift index cb9d628..816e8f3 100644 --- a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeRecentRunsView.swift +++ b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeRecentRunsView.swift @@ -5,7 +5,7 @@ struct CoreAIRuntimeRecentRunsView: View { var body: some View { if !coordinator.history.isEmpty { - Section("Recent Runtime Runs") { + Section { ForEach(coordinator.history.prefix(8)) { run in VStack(alignment: .leading) { Label( @@ -26,6 +26,8 @@ struct CoreAIRuntimeRecentRunsView: View { } .accessibilityElement(children: .combine) } + } header: { + Label("Recent Runtime Runs", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90") } } } diff --git a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeRecordingControlsView.swift b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeRecordingControlsView.swift index b9d79af..1192f19 100644 --- a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeRecordingControlsView.swift +++ b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeRecordingControlsView.swift @@ -6,7 +6,7 @@ struct CoreAIRuntimeRecordingControlsView: View { @Bindable var coordinator: CoreAIRunLifecycleCoordinator var body: some View { - Section("Run Recording") { + Section { Picker("Record in project", selection: $selectedProjectID) { Text("Off") .tag(nil as UUID?) @@ -29,17 +29,32 @@ struct CoreAIRuntimeRecordingControlsView: View { ForEach(coordinator.comparisonOptions) { identity in Text(identity.displayName) .tag(identity as CoreAIRuntimeComparisonIdentity?) - } + } } - Text("Attempts remain cold until one run succeeds for the imported model in this Runtime Studio session; later runs with the same experience and model identity are warm.") - .foregroundStyle(.secondary) + DisclosureGroup { + VStack(alignment: .leading) { + Label("Cold and Warm Timing", systemImage: "thermometer.variable") + .bold() + Text("Attempts remain cold until one run succeeds for the imported model in this Runtime Studio session. Later runs with the same experience and model identity are warm.") - Text("A comparison identity records the intended comparator only; this slice does not claim that outputs were compared.") - .foregroundStyle(.secondary) + Divider() + + Label("Comparison Identity", systemImage: "arrow.left.arrow.right") + .bold() + Text("A comparison identity records the intended comparator only. It does not claim that outputs were compared.") - Text("A registry recipe is an import intent. Runtime Studio checks the imported model family, but records unverified_intent and does not link a project recipe revision without artifact-bound provenance proof.") + Divider() + + Label("Recipe Provenance", systemImage: "checkmark.seal") + .bold() + Text("A registry recipe is an import intent. Runtime Studio records unverified_intent until artifact-bound provenance proves the project recipe revision.") + } + .font(.footnote) .foregroundStyle(.secondary) + } label: { + Label("How Run Evidence Works", systemImage: "info.circle") + } if let persistenceMessage = coordinator.persistenceMessage { Label(persistenceMessage, systemImage: "exclamationmark.triangle") @@ -53,6 +68,8 @@ struct CoreAIRuntimeRecordingControlsView: View { action: coordinator.retryPendingPersistence ) } + } header: { + Label("Run Recording", systemImage: "record.circle") } } } diff --git a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeStudioView.swift b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeStudioView.swift index cf0caa3..b4fffac 100644 --- a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeStudioView.swift +++ b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeStudioView.swift @@ -15,12 +15,12 @@ struct CoreAIRuntimeStudioView: View { Group { if let loadError = model.loadError { ContentUnavailableView( - "Runtime Registry Unavailable", + "Couldn't Load Runtime Experiences", systemImage: "exclamationmark.triangle", description: Text(loadError) ) } else if model.registry == nil { - ProgressView("Loading Runtime Studio…") + ProgressView("Loading runtime experiences…") } else if model.filteredMappings.isEmpty { ContentUnavailableView.search } else { @@ -39,6 +39,7 @@ struct CoreAIRuntimeStudioView: View { ) } } + .listStyle(.inset) } } .navigationTitle("Runtime Studio") @@ -66,11 +67,11 @@ struct CoreAIRuntimeStudioView: View { ) } else { ContentUnavailableView( - "Experience Unavailable", + "Experience Not Found", systemImage: "questionmark.folder", description: Text(route.unavailableDescription) ) - .navigationTitle("Experience Unavailable") + .navigationTitle("Experience Not Found") } } .task { diff --git a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeUnsupportedExperienceView.swift b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeUnsupportedExperienceView.swift index fcb8c0c..c1fcab4 100644 --- a/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeUnsupportedExperienceView.swift +++ b/CoreAILab/Features/RuntimeStudio/CoreAIRuntimeUnsupportedExperienceView.swift @@ -5,10 +5,10 @@ struct CoreAIRuntimeUnsupportedExperienceView: View { var body: some View { ContentUnavailableView( - "Experience Adapter Unavailable", + "Adapter Not Available in This Build", systemImage: "exclamationmark.triangle", description: Text( - "The recipe maps \(mapping.experience.modelIdentifier) to \(mapping.experience.adapter.rawValue), but this build cannot resolve that model preset." + "The recipe maps \(mapping.experience.modelIdentifier) to \(mapping.experience.adapter.title), but this build cannot resolve that model preset." ) ) .navigationTitle(mapping.experience.title) diff --git a/CoreAILabCore/RuntimeStudio/CoreAIExperienceAdapter.swift b/CoreAILabCore/RuntimeStudio/CoreAIExperienceAdapter.swift index 07bb898..dfc3505 100644 --- a/CoreAILabCore/RuntimeStudio/CoreAIExperienceAdapter.swift +++ b/CoreAILabCore/RuntimeStudio/CoreAIExperienceAdapter.swift @@ -8,6 +8,23 @@ enum CoreAIExperienceAdapter: String, Codable, Hashable, Sendable { case appleSegmentation case genericFunctionWorkbench + var title: String { + switch self { + case .appleAudioTranscription: + "Apple Audio Transcription" + case .appleDiffusion: + "Apple Diffusion" + case .appleLanguage: + "Apple Language" + case .appleObjectDetection: + "Apple Object Detection" + case .appleSegmentation: + "Apple Segmentation" + case .genericFunctionWorkbench: + "Function Workbench" + } + } + func supports(_ workload: CoreAIExperienceWorkload) -> Bool { switch self { case .appleAudioTranscription: diff --git a/CoreAILabTests/CoreAIExperienceRegistryTests.swift b/CoreAILabTests/CoreAIExperienceRegistryTests.swift index 30d6e7b..4e20995 100644 --- a/CoreAILabTests/CoreAIExperienceRegistryTests.swift +++ b/CoreAILabTests/CoreAIExperienceRegistryTests.swift @@ -158,7 +158,7 @@ struct CoreAIExperienceRegistryTests { ) #expect(route.unavailableDescription.contains("missing-experience")) - #expect(route.unavailableDescription.contains("current runtime registry")) + #expect(route.unavailableDescription.contains("runtime registry")) } private func makeManifest( diff --git a/CoreAILabTests/CoreAIFunctionBenchmarkTests.swift b/CoreAILabTests/CoreAIFunctionBenchmarkTests.swift index 0007ac2..c0bfb95 100644 --- a/CoreAILabTests/CoreAIFunctionBenchmarkTests.swift +++ b/CoreAILabTests/CoreAIFunctionBenchmarkTests.swift @@ -372,7 +372,12 @@ struct CoreAIFunctionBenchmarkTests { @Test func realFixtureRunsWarmupAndMeasuredInference() async throws { let service = CoreAISpecializationService() - let fixtureURL = try CoreAITestFixtures.tensorModelURL() + let fixtureURL = try CoreAITestFixtures.temporaryTensorModelURL() + defer { + try? FileManager.default.removeItem( + at: fixtureURL.deletingLastPathComponent() + ) + } try? await service.removeCachedEntries(at: fixtureURL) do { diff --git a/CoreAILabTests/CoreAIFunctionWorkbenchTests.swift b/CoreAILabTests/CoreAIFunctionWorkbenchTests.swift index c8ed112..3bd5608 100644 --- a/CoreAILabTests/CoreAIFunctionWorkbenchTests.swift +++ b/CoreAILabTests/CoreAIFunctionWorkbenchTests.swift @@ -207,7 +207,12 @@ struct CoreAIFunctionWorkbenchTests { @Test func realCoreAIFixtureRunsFloatAndIntegerFunctions() async throws { let service = CoreAISpecializationService() - let fixtureURL = try CoreAITestFixtures.tensorModelURL() + let fixtureURL = try CoreAITestFixtures.temporaryTensorModelURL() + defer { + try? FileManager.default.removeItem( + at: fixtureURL.deletingLastPathComponent() + ) + } try? await service.removeCachedEntries(at: fixtureURL) do { diff --git a/CoreAILabTests/CoreAIIntegrationExportTests.swift b/CoreAILabTests/CoreAIIntegrationExportTests.swift index bfff896..5c2e914 100644 --- a/CoreAILabTests/CoreAIIntegrationExportTests.swift +++ b/CoreAILabTests/CoreAIIntegrationExportTests.swift @@ -595,6 +595,7 @@ struct CoreAIIntegrationExportTests { func cleanExportVerifierBuildsTheStandalonePackage() async throws { let exportParent = temporaryDirectory() let cleanParent = temporaryDirectory() + let developerDirectory = try activeDeveloperDirectory() defer { try? FileManager.default.removeItem(at: exportParent) try? FileManager.default.removeItem(at: cleanParent) @@ -614,7 +615,7 @@ struct CoreAIIntegrationExportTests { at: URL(filePath: "/usr/bin/python3"), arguments: [cleanPackageURL.appending(path: "verify-export.py").path], environment: [ - "DEVELOPER_DIR": "/Applications/Xcode-beta.app/Contents/Developer", + "DEVELOPER_DIR": developerDirectory, ] ) #expect(output.contains("Core AI integration export verified.")) @@ -682,7 +683,7 @@ struct CoreAIIntegrationExportTests { "--disable-automatic-resolution", ], environment: [ - "DEVELOPER_DIR": "/Applications/Xcode-beta.app/Contents/Developer", + "DEVELOPER_DIR": developerDirectory, "SWIFTPM_DISABLE_PACKAGE_REPOSITORY_CACHE": "1", ] ) @@ -752,7 +753,7 @@ struct CoreAIIntegrationExportTests { "build", ], environment: [ - "DEVELOPER_DIR": "/Applications/Xcode-beta.app/Contents/Developer", + "DEVELOPER_DIR": developerDirectory, "SWIFTPM_DISABLE_PACKAGE_REPOSITORY_CACHE": "1", ], currentDirectoryURL: iOSConsumerURL @@ -850,6 +851,17 @@ struct CoreAIIntegrationExportTests { URL.temporaryDirectory.appending(path: UUID().uuidString, directoryHint: .isDirectory) } + private func activeDeveloperDirectory() throws -> String { + if let developerDirectory = ProcessInfo.processInfo.environment["DEVELOPER_DIR"], + FileManager.default.fileExists(atPath: developerDirectory) { + return developerDirectory + } + return try runExecutable( + at: URL(filePath: "/usr/bin/xcode-select"), + arguments: ["--print-path"] + ).trimmingCharacters(in: .whitespacesAndNewlines) + } + private func packageSnapshot(at rootURL: URL) throws -> [String: Data] { guard let enumerator = FileManager.default.enumerator( at: rootURL, diff --git a/CoreAILabTests/CoreAITestFixtures.swift b/CoreAILabTests/CoreAITestFixtures.swift index edd9358..c55410b 100644 --- a/CoreAILabTests/CoreAITestFixtures.swift +++ b/CoreAILabTests/CoreAITestFixtures.swift @@ -55,6 +55,30 @@ enum CoreAITestFixtures { return modelURL } + static func temporaryTensorModelURL() throws -> URL { + let sourceURL = try tensorModelURL() + let directoryURL = URL.temporaryDirectory.appending( + path: UUID().uuidString, + directoryHint: .isDirectory + ) + let modelURL = directoryURL.appending( + path: sourceURL.lastPathComponent, + directoryHint: .isDirectory + ) + + do { + try FileManager.default.createDirectory( + at: directoryURL, + withIntermediateDirectories: true + ) + try FileManager.default.copyItem(at: sourceURL, to: modelURL) + return modelURL + } catch { + try? FileManager.default.removeItem(at: directoryURL) + throw error + } + } + static func diarizationFeatureURL() throws -> URL { let bundle = Bundle(for: CoreAITestBundleToken.self) guard let url = bundle.url( diff --git a/CoreAILabTests/SpeakerDiarizationLabTests.swift b/CoreAILabTests/SpeakerDiarizationLabTests.swift index 0754461..a6d8f8c 100644 --- a/CoreAILabTests/SpeakerDiarizationLabTests.swift +++ b/CoreAILabTests/SpeakerDiarizationLabTests.swift @@ -31,6 +31,43 @@ struct SpeakerDiarizationLabTests { workspace.cancelWork() } + @Test + @MainActor + func replacementKeepsCurrentMediaVisibleWhileCandidateIsAnalyzed() async throws { + let currentURL = FileManager.default.temporaryDirectory.appending( + path: "diarization-current-\(UUID().uuidString).wav" + ) + let missingURL = FileManager.default.temporaryDirectory.appending( + path: "diarization-missing-\(UUID().uuidString).wav" + ) + defer { try? FileManager.default.removeItem(at: currentURL) } + try writeSineWave(to: currentURL) + + let workspace = SpeakerDiarizationWorkspaceModel( + engine: SpeakerDiarizationServiceFake() + ) + workspace.selectMedia(currentURL) + while workspace.isAnalyzingMedia { + await Task.yield() + } + let currentSummary = try #require(workspace.mediaSummary) + let currentWaveform = try #require(workspace.waveform) + + workspace.selectMedia(missingURL) + + #expect(workspace.isAnalyzingMedia) + #expect(workspace.mediaURL == currentURL) + #expect(workspace.mediaSummary == currentSummary) + #expect(workspace.waveform == currentWaveform) + + while workspace.isAnalyzingMedia { + await Task.yield() + } + #expect(workspace.mediaURL == currentURL) + #expect(workspace.mediaSummary == currentSummary) + #expect(workspace.waveform == currentWaveform) + } + @Test func bundledCAMPlusHasPinnedLicenseProvenanceAndContract() async throws { let modelURL = try SpeakerDiarizationBundledModel.url() @@ -66,18 +103,6 @@ struct SpeakerDiarizationLabTests { #expect(info.scalarTypeName == "float16") } - @Test - func analysisLayoutUsesAContentDrivenBreakpoint() { - let breakpoint = SpeakerDiarizationAnalysisLayout.minimumSideBySideWidth - - #expect( - SpeakerDiarizationAnalysisLayout(contentWidth: breakpoint - 1) == .stacked - ) - #expect( - SpeakerDiarizationAnalysisLayout(contentWidth: breakpoint) == .sideBySide - ) - } - @Test func energySegmentationFindsSeparatedSpeechAndRejectsSilence() { let sampleRate = 1_000 diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..421c135 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,168 @@ +--- +name: Core AI Lab +description: A precise native workbench for evidence-backed Core AI workflows. +colors: + system-blue: "#007AFF" + system-green: "#34C759" + system-orange: "#FF9500" + system-red: "#FF3B30" + on-accent: "#FFFFFF" +typography: + title: + fontFamily: "SF Pro, -apple-system, BlinkMacSystemFont, sans-serif" + fontSize: "1.25rem" + fontWeight: 600 + lineHeight: 1.25 + letterSpacing: "normal" + body: + fontFamily: "SF Pro, -apple-system, BlinkMacSystemFont, sans-serif" + fontSize: "1rem" + fontWeight: 400 + lineHeight: 1.4 + letterSpacing: "normal" + technical: + fontFamily: "SF Mono, ui-monospace, SFMono-Regular, monospace" + fontSize: "0.875rem" + fontWeight: 400 + lineHeight: 1.4 + letterSpacing: "normal" +components: + button-primary: + backgroundColor: "{colors.system-blue}" + textColor: "{colors.on-accent}" + typography: "{typography.body}" + status-success: + textColor: "{colors.system-green}" + typography: "{typography.body}" + status-warning: + textColor: "{colors.system-orange}" + typography: "{typography.body}" + status-error: + textColor: "{colors.system-red}" + typography: "{typography.body}" +--- + +# Design System: Core AI Lab + +## Overview + +**Creative North Star: “The Native Instrument”** + +Core AI Lab should feel like an Apple developer instrument: calm at rest, exact when interrogated, and candid about what it knows. The interface uses standard SwiftUI navigation, forms, lists, tables, toolbars, and status views so platform behavior carries the visual language. Technical density is welcome when it improves comparison or preserves evidence; decoration is not a substitute for hierarchy. + +Each workspace follows a legible sequence: orient, prepare, act, observe, verify. Common actions stay visible while prerequisites, provenance, raw identifiers, and limitations appear where they become relevant. Standard controls inherit the platform's current materials and Liquid Glass behavior automatically; the app does not add ornamental glass effects of its own. + +The system explicitly rejects consumer chat-app framing, generic dashboard card grids, science-fiction control panels, and marketing-first presentation. It should remain recognizably native on iPhone, iPad, and Mac while preserving the rigor expected from a model inspection and validation tool. + +**Key Characteristics:** + +- Native and platform-adaptive +- Evidence-led rather than decorative +- Information-rich with progressive disclosure +- Clear about prerequisites, progress, cancellation, and failure +- Accessible by default, including keyboard and VoiceOver workflows + +## Colors + +Color is semantic and restrained. SwiftUI system backgrounds, labels, separators, fills, and materials are the source of truth for light mode, dark mode, Increase Contrast, and platform variation. + +### Primary + +- **System Blue:** The sole interactive accent for selection, links, focus, and the primary action in a workflow. + +### Status + +- **System Green:** Verified success or a completed, valid state. Pair it with a checkmark and text. +- **System Orange:** Caution, provisional evidence, or a condition that deserves review. Pair it with a warning symbol and explanation. +- **System Red:** Destructive actions and failures that need attention. Never use it as ambient decoration. + +### Neutral + +- Use semantic SwiftUI styles such as primary, secondary, tertiary, background, grouped background, separator, and material. Do not hardcode neutral colors that would break system appearance modes. + +**The One Accent Rule.** A screen gets one prominent blue action. Secondary actions use standard bordered, plain, menu, or navigation treatments. + +**The Redundancy Rule.** Status is always expressed with text and an icon or shape in addition to color. + +## Typography + +- **Display Font:** SF Pro through SwiftUI semantic title styles +- **Body Font:** SF Pro through SwiftUI semantic body, callout, subheadline, and footnote styles +- **Label/Mono Font:** SF Mono through monospaced variants of the nearest semantic style + +**Character:** Familiar, highly legible, and quiet. Typography creates hierarchy without oversized display treatments or gratuitous weight changes. + +### Hierarchy + +- **Title:** Navigation titles and the occasional workspace identity. Prefer the system navigation title before adding an in-content title. +- **Headline:** Section-leading labels and a small number of meaningful summaries. +- **Body:** Primary instructions, results, and user-authored content. Allow Dynamic Type to determine the runtime size. +- **Subheadline / Footnote:** Evidence boundaries, provenance, prerequisites, and supporting detail. +- **Technical:** Commands, hashes, paths, identifiers, tensor shapes, and numeric evidence. Keep them selectable and truncate long identifiers in the middle when horizontal space is limited. + +**The Semantic Type Rule.** Use SwiftUI semantic styles rather than fixed point sizes. Bold is reserved for hierarchy, not routine emphasis. + +## Elevation + +The system is flat by default. Depth comes from platform navigation, grouped form backgrounds, sheets, popovers, menus, and system materials rather than custom shadows. Liquid Glass belongs to the navigation and control layer supplied by SwiftUI; content and evidence surfaces remain solid and readable. A material may back a temporary progress overlay, but it must not become a decorative card treatment. + +**The Platform Depth Rule.** If SwiftUI already communicates the layer, do not add another shadow, blur, stroke, or floating container. + +## Components + +### Navigation + +- Use `NavigationSplitView` for the app shell and group destinations as Library, Build, Run, and Validate. +- Preserve the selected destination, supply a symbol and concise accessibility hint, and let the system manage sidebar selection and toolbar overflow. +- Use a nested sidebar only when a tool has a genuine second-level information architecture, such as Recipe Studio. + +### Grouped Workspaces + +- Use grouped `Form` sections for ordered technical workflows. Every meaningful section has a concise noun label and, where useful, a familiar SF Symbol. +- Lead with current state and prerequisites, then inputs, the primary action, results, and evidence. +- Prefer full-width readable content over grids of small cards. + +### Buttons + +- Use an active verb and a familiar symbol. One action per workflow may use `borderedProminent`; supporting actions remain standard. +- Keep destructive actions in menus, confirmation dialogs, or explicit destructive roles unless immediate visibility is essential. +- While an action runs, show a labeled `ProgressView` that names the current work. Do not leave an active button looking tappable. +- Maintain a minimum 44 by 44 point interactive target on touch platforms and preserve standard keyboard shortcuts on Mac. + +### Inputs and Search + +- Use native fields, pickers, steppers, sliders, file importers, and search. Labels describe the value, while footers explain constraints or consequences. +- Disable an input only when editing it would be invalid during the current state; never use disabled styling as a substitute for an explanation. + +### Empty, Loading, and Failure States + +- Use `ContentUnavailableView` for first-run guidance, empty results, unsupported capabilities, and missing content. +- Loading states name the operation and show progress. Failure titles state what could not be completed; the message gives the exact error or a useful recovery action. +- Treat user-canceled pickers as cancellation, not failure. + +### Evidence and Results + +- Use `LabeledContent`, disclosure groups, lists, and tables for comparable facts. Keep raw commands, hashes, paths, and identifiers monospaced and selectable. +- Distinguish preferences, plans, cache states, and measured evidence in both layout and copy. +- Collapse explanatory detail when the primary task would otherwise be buried, but never hide caveats required to interpret a result honestly. + +## Do's and Don'ts + +### Do: + +- **Do** start every workspace with purpose, current state, and the next meaningful action. +- **Do** rely on standard SwiftUI controls so appearance, Liquid Glass, keyboard behavior, and accessibility adapt with the platform. +- **Do** use one prominent primary action and let toolbars contain only high-value, contextual commands. +- **Do** use Dynamic Type, semantic colors, text-plus-symbol status, and 44-point touch targets. +- **Do** write concise sentence-case labels and task-specific failures such as “Couldn't Import the Recipe Bundle.” +- **Do** keep technical evidence precise, selectable, and visually quieter than the user's current task. + +### Don't: + +- **Don't** imitate consumer AI chat apps that reduce every workflow to a prompt box. +- **Don't** build generic SaaS dashboards from interchangeable card grids and decorative metrics. +- **Don't** use sci-fi control panels with neon gradients, ornamental glass, or unexplained status lights. +- **Don't** create marketing surfaces that hide provenance, prerequisites, or limitations behind optimistic copy. +- **Don't** introduce bespoke controls that replace familiar Apple platform behavior without improving the task. +- **Don't** use color alone, static hourglass symbols, vague “Operation Failed” alerts, or user-visible raw enum values. +- **Don't** claim hardware placement, performance, cache benefit, or model availability without corresponding evidence. diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..e0ab78e --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,37 @@ +# Product + +## Register + +product + +## Users + +Apple-platform developers, machine-learning engineers, and framework researchers who need to understand and validate Core AI assets on iPhone, iPad, and Mac. They work through technical, multi-step workflows and need the interface to preserve context while they compare recipes, prepare assets, run models, inspect evidence, and package results. + +## Product Purpose + +Core AI Lab is a native workbench for discovering, converting, inspecting, running, benchmarking, and packaging models with Apple's `CoreAI.framework`. It succeeds when a developer can move from an unfamiliar asset or recipe to a reproducible, evidence-backed result without mistaking a preference, cached artifact, or conversion plan for measured runtime truth. + +## Brand Personality + +Precise, native, and quietly technical. The app should feel like a trustworthy Apple developer instrument: capable without theatrics, information-rich without becoming dense, and candid about prerequisites, progress, and failure. + +## Anti-references + +- Consumer AI chat apps that reduce every workflow to a prompt box. +- Generic SaaS dashboards built from interchangeable card grids and decorative metrics. +- Sci-fi control panels with neon gradients, ornamental glass, or unexplained status lights. +- Marketing surfaces that hide provenance, prerequisites, or limitations behind optimistic copy. +- Bespoke controls that replace familiar Apple platform behavior without improving the task. + +## Design Principles + +1. **Orient before acting.** Every workspace makes its purpose, current state, prerequisites, and next meaningful action clear. +2. **Progressively disclose complexity.** Lead with the common path, then reveal technical detail where it becomes relevant. +3. **Evidence earns emphasis.** Give measured results, provenance, and validation findings stronger hierarchy than decoration or unverified capability claims. +4. **Use platform fluency.** Prefer familiar navigation, controls, keyboard behavior, and system feedback so the tool disappears into the work. +5. **Keep state honest.** Loading, unsupported, cancelled, empty, warning, and failure states remain distinct and actionable. + +## Accessibility & Inclusion + +Follow Apple's accessibility guidance and target WCAG 2.2 AA contrast for custom treatments. Support Dynamic Type, VoiceOver, keyboard and pointer navigation, Reduce Motion, Increase Contrast, and Differentiate Without Color. Status and selection must never rely on color alone, touch targets on iOS must be at least 44 by 44 points, and technical language must remain understandable without hiding exact evidence from expert users.