Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Nativ.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
0AB78A4BB9F2D08433653C7C /* HuggingFaceCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A832F692EF1E810D78BD6B6C /* HuggingFaceCacheTests.swift */; };
0B11412D6A9FB77B9A204E7C /* Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D808CCA7AE53C7245C3D7 /* Formatting.swift */; };
0B5170A046C3C587DE077072 /* PDFDocumentTextExtractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8513AE482EB6C3113080826A /* PDFDocumentTextExtractorTests.swift */; };
0BE40CBDC8E2F00EDED01E21 /* ArtifactCatalogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2763077B30E27C9B7BB3E47F /* ArtifactCatalogTests.swift */; };
0C9F43690B51C4F3012E9DD2 /* ControlPanelSidebarDragAndDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 087534BC45A233134507ED19 /* ControlPanelSidebarDragAndDrop.swift */; };
0CEEC986F8617E501223DA3E /* NativExtensionMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941186B3A4BA46FB90201DE /* NativExtensionMessaging.swift */; };
0DF842E2BE07D345918F3D54 /* Manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 5AF8F6359E4523814FB0EFDA /* Manifest.json */; };
Expand Down Expand Up @@ -396,6 +397,7 @@
C5CE4EBDB988CF611FB2D6DA /* SystemMenuBarPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE1E26171E964D9769E65CB /* SystemMenuBarPreferences.swift */; };
C6DFA4F348008EDB924A3FC1 /* WebReadContentSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F15F7E180170C5FA55BD38F /* WebReadContentSelector.swift */; };
C7697F50D8F4E4628DFEE397 /* Tavily.png in Resources */ = {isa = PBXBuildFile; fileRef = BF65681C9FD920314BECC7D3 /* Tavily.png */; };
C7E82815819A49D1AA619711 /* ArtifactCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14764324568DE9C6AC095EDA /* ArtifactCatalog.swift */; };
C84FA1A269F12B130EAEB57B /* ModelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EB7572F271F7D68DC3F97D /* ModelsView.swift */; };
C89558DC65CAAEC956198688 /* ExtensionsHubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D5BCE1A4769FE7823B5C79D /* ExtensionsHubView.swift */; };
C8A1887418039B9DDBCE1226 /* NativWindowRoot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7D407CD0B6A6F5440AE923 /* NativWindowRoot.swift */; };
Expand All @@ -416,6 +418,7 @@
D51D08482F5C0E76ACFEFDB1 /* ChatImageTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EF3E0B4A9EEC293EFD5F8A /* ChatImageTool.swift */; };
D635FD9C6938DC35CC56CAD7 /* NativApplicationCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1450610DDB81712B1F7F02B6 /* NativApplicationCommands.swift */; };
D63A4CDF0E69DFAF43C5B12F /* ChatSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 530D284E65297E3CA8050AE1 /* ChatSessionStore.swift */; };
D6B37FDD3002F281B6BF9DA5 /* ArtifactCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14764324568DE9C6AC095EDA /* ArtifactCatalog.swift */; };
D74F92EBE05FADCD813CD8B9 /* AudioAnalyticsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7E0BD007DAD141D50E1BAA4 /* AudioAnalyticsStore.swift */; };
D76DC0072E523E6409DF1176 /* cmark-gfm in Frameworks */ = {isa = PBXBuildFile; productRef = 8AFF466EA672EAA5E86CA73F /* cmark-gfm */; };
D835C99B486C3FE352A27039 /* ChatMarkdownRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432F8E84739652B2546DC7B7 /* ChatMarkdownRenderer.swift */; };
Expand Down Expand Up @@ -592,6 +595,7 @@
0F5BF52CED5DE873DD6BF02B /* CodexCLIProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexCLIProfile.swift; sourceTree = "<group>"; };
141A3E7402D8AE90971C374B /* ModelPrimaryTaskResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelPrimaryTaskResolver.swift; sourceTree = "<group>"; };
1450610DDB81712B1F7F02B6 /* NativApplicationCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativApplicationCommands.swift; sourceTree = "<group>"; };
14764324568DE9C6AC095EDA /* ArtifactCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtifactCatalog.swift; sourceTree = "<group>"; };
14F98763452E5A3BED006155 /* ChatFileWriteTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFileWriteTools.swift; sourceTree = "<group>"; };
151A0F6AD81E505ADF4C8BE2 /* MarkdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
1573A2D88784C9D404156D25 /* VoiceDictationExtensionRuntime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceDictationExtensionRuntime.swift; sourceTree = "<group>"; };
Expand All @@ -615,6 +619,7 @@
254FDFBF7C5F7C8019E9E03F /* WebBrowsingRuntime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBrowsingRuntime.swift; sourceTree = "<group>"; };
257CE2FD6DEE59CA174E9E17 /* ChatTranscriptPresentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTranscriptPresentationTests.swift; sourceTree = "<group>"; };
2580C52ECE6D40D71E44463F /* WebSearchProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSearchProvider.swift; sourceTree = "<group>"; };
2763077B30E27C9B7BB3E47F /* ArtifactCatalogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtifactCatalogTests.swift; sourceTree = "<group>"; };
27D63523BA62CC95FDD8D51E /* FnControlShortcutMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FnControlShortcutMonitor.swift; sourceTree = "<group>"; };
27F42CEC1B39477AC13352E3 /* NativExtensionStateStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativExtensionStateStore.swift; sourceTree = "<group>"; };
2A1B515ECDF21B3616E5A3D1 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1501,6 +1506,7 @@
isa = PBXGroup;
children = (
8BB9D1D0FC24AC771442B628 /* Artifact.swift */,
14764324568DE9C6AC095EDA /* ArtifactCatalog.swift */,
811B9BB1926AD34A02556012 /* ArtifactPreview.swift */,
A55A33BE26A6AF77B69C0430 /* ArtifactSearchIndex.swift */,
46BCF5F68C499E27A789A91F /* ArtifactSemanticSearch.swift */,
Expand Down Expand Up @@ -1565,6 +1571,7 @@
F1DA823EC3D3358ABDB845B7 /* NativTests */ = {
isa = PBXGroup;
children = (
2763077B30E27C9B7BB3E47F /* ArtifactCatalogTests.swift */,
FE4FFC04214D6E98258786D8 /* ArtifactDeletionTests.swift */,
FD136B8C2C1F6B2B554064C5 /* ArtifactRefreshTests.swift */,
3B965B701714787B0A85AE6B /* AudioTests.swift */,
Expand Down Expand Up @@ -1930,6 +1937,7 @@
B15353CDE6204176664DB3B2 /* AppDelegate.swift in Sources */,
826AB68248F3DD8952284745 /* AppleSpeechTranscriber.swift in Sources */,
A9B82EFAD4FD8C57E045721B /* Artifact.swift in Sources */,
D6B37FDD3002F281B6BF9DA5 /* ArtifactCatalog.swift in Sources */,
B63E4B683EE9CA383DBEFADA /* ArtifactPreview.swift in Sources */,
5FE38D5A9D40E133EC25BA20 /* ArtifactSearchIndex.swift in Sources */,
0935A0AC0D6A6298D551EEAE /* ArtifactSemanticSearch.swift in Sources */,
Expand Down Expand Up @@ -2148,6 +2156,8 @@
buildActionMask = 2147483647;
files = (
145464A2B098437667795AFB /* Artifact.swift in Sources */,
C7E82815819A49D1AA619711 /* ArtifactCatalog.swift in Sources */,
0BE40CBDC8E2F00EDED01E21 /* ArtifactCatalogTests.swift in Sources */,
4262747AC6425403FC1F2210 /* ArtifactDeletionTests.swift in Sources */,
3CFCC551774CD33CA71FE609 /* ArtifactRefreshTests.swift in Sources */,
3D96ED3F0E463F75A47071C6 /* ArtifactStore.swift in Sources */,
Expand Down
44 changes: 42 additions & 2 deletions Sources/Nativ/Features/Artifacts/Artifact.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ enum ArtifactKind: String, CaseIterable, Codable, Identifiable {
}
}

enum ArtifactSource: String, CaseIterable, Codable, Identifiable {
enum ArtifactSource: String, CaseIterable, Codable, Identifiable, Sendable {
case uploaded
case generated
case unknown

var id: String { rawValue }

Expand All @@ -71,6 +72,8 @@ enum ArtifactSource: String, CaseIterable, Codable, Identifiable {
"Uploaded"
case .generated:
"Generated"
case .unknown:
"Unknown"
}
}

Expand All @@ -80,14 +83,37 @@ enum ArtifactSource: String, CaseIterable, Codable, Identifiable {
"square.and.arrow.up"
case .generated:
"sparkles"
case .unknown:
"questionmark.circle"
}
}
}

struct ArtifactGeneration: Codable, Equatable, Sendable {
var prompt: String?
var modelID: String?
var seed: Int?
var width: Int?
var height: Int?
}

struct ArtifactUsage: Codable, Equatable, Hashable, Sendable {
enum Workspace: String, Codable, Sendable {
case chat
case imageGeneration
}

let workspace: Workspace
let sessionID: UUID
let messageID: UUID?
let attachmentID: UUID
var isGenerationOutput: Bool = false
}

struct Artifact: Identifiable, Codable, Equatable {
let id: UUID
let kind: ArtifactKind
let source: ArtifactSource
var source: ArtifactSource
let sessionID: UUID
let messageID: UUID
let filename: String
Expand All @@ -97,6 +123,20 @@ struct Artifact: Identifiable, Codable, Equatable {
let createdAt: Date
let prompt: String?
let sessionTitle: String
var asset: MediaAssetReference? = nil
var generation: ArtifactGeneration? = nil
var usages: [ArtifactUsage]? = nil

var locations: [ArtifactUsage] {
usages ?? [ArtifactUsage(
workspace: source == .generated ? .imageGeneration : .chat,
sessionID: sessionID, messageID: messageID, attachmentID: id
)]
}

var workspace: ArtifactUsage.Workspace {
locations.first?.workspace ?? .chat
}

var fileExtension: String {
(filename as NSString).pathExtension.uppercased()
Expand Down
137 changes: 137 additions & 0 deletions Sources/Nativ/Features/Artifacts/ArtifactCatalog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import Foundation

enum ArtifactCatalog {
static func artifacts(chats: [ChatSession], images: [ImageGenerationSession]) -> [Artifact] {
var entries: [Artifact] = []
for session in chats {
for message in session.messages {
for attachment in message.imageAttachments {
let generation = message.artifactGeneration(for: attachment)
append(
attachment, generation: generation,
usage: ArtifactUsage(workspace: .chat, sessionID: session.id,
messageID: message.id, attachmentID: attachment.id,
isGenerationOutput: message.isImageGenerationResult),
title: session.displayTitle, date: message.createdAt,
prompt: generation?.prompt ?? message.content, to: &entries
)
}
}
}
for session in images {
if let reference = session.activeReference {
append(
reference, generation: reference.generation,
usage: ArtifactUsage(workspace: .imageGeneration, sessionID: session.id,
messageID: nil, attachmentID: reference.id),
title: session.displayTitle, date: session.createdAt, prompt: nil, to: &entries
)
}
for turn in session.turns {
for reference in turn.referenceImages {
append(
reference, generation: reference.generation,
usage: ArtifactUsage(workspace: .imageGeneration, sessionID: session.id,
messageID: turn.id, attachmentID: reference.id),
title: session.displayTitle, date: turn.createdAt, prompt: nil, to: &entries
)
}
for output in turn.outputs {
var attachment = output.attachment
attachment.generation?.prompt = output.revisedPrompt ?? turn.prompt
attachment.generation?.modelID = turn.modelID
append(
attachment, generation: attachment.generation,
usage: ArtifactUsage(workspace: .imageGeneration, sessionID: session.id,
messageID: turn.id, attachmentID: output.id, isGenerationOutput: true),
title: session.displayTitle, date: turn.createdAt,
prompt: attachment.generation?.prompt, to: &entries
)
}
}
}
let groups = Dictionary(grouping: entries, by: \.id)
return groups.values.compactMap { group in
let ordered = group.sorted {
if ($0.source != .unknown) != ($1.source != .unknown) {
return $0.source != .unknown
}
if ($0.generation != nil) != ($1.generation != nil) {
return $0.generation != nil
}
if $0.locations[0].isGenerationOutput != $1.locations[0].isGenerationOutput {
return $0.locations[0].isGenerationOutput
}
if $0.createdAt != $1.createdAt { return $0.createdAt < $1.createdAt }
if ($0.locations[0].messageID != nil) != ($1.locations[0].messageID != nil) {
return $0.locations[0].messageID != nil
}
return $0.sessionID.uuidString < $1.sessionID.uuidString
}
guard var artifact = ordered.first else { return nil }
let origins = Set(group.map(\.source).filter { $0 != .unknown })
if origins.count > 1 { artifact.source = .unknown }
var seen: Set<ArtifactUsage> = []
artifact.usages = ordered.flatMap(\.locations).filter { seen.insert($0).inserted }
return artifact
}.sorted {
if $0.createdAt != $1.createdAt { return $0.createdAt > $1.createdAt }
return $0.id.uuidString < $1.id.uuidString
}
}

private static func append(
_ attachment: ChatImageAttachment, generation: ArtifactGeneration?, usage: ArtifactUsage,
title: String, date: Date, prompt: String?, to entries: inout [Artifact]
) {
guard let asset = attachment.asset else { return }
entries.append(Artifact(
id: attachment.assetID,
kind: ArtifactKind.resolve(mimeType: attachment.mimeType, filename: attachment.filename),
source: attachment.origin ?? (generation == nil ? .unknown : .generated),
sessionID: usage.sessionID, messageID: usage.messageID ?? usage.sessionID,
filename: attachment.filename, mimeType: attachment.mimeType,
relativePath: asset.relativePath, byteSize: asset.byteCount, createdAt: date,
prompt: prompt?.isEmpty == false ? prompt : nil, sessionTitle: title,
asset: asset, generation: generation, usages: [usage]
))
}
}

extension ChatSession {
mutating func removeArtifact(_ assetID: UUID) {
for index in messages.indices {
messages[index].imageAttachments.removeAll { $0.assetID == assetID }
}
}
}

extension ImageGenerationSession {
mutating func removeArtifact(_ assetID: UUID) {
for index in turns.indices {
turns[index].referenceImages.removeAll { $0.assetID == assetID }
turns[index].outputs.removeAll { ($0.asset?.id ?? $0.id) == assetID }
}
if activeReference?.assetID == assetID { activeReference = nil }
}
}

enum ArtifactDeletion {
static func removeReferences(
to artifact: Artifact,
isActive: (ArtifactUsage.Workspace, UUID) -> Bool,
remove: (ArtifactUsage.Workspace, UUID) -> Bool
) -> Bool {
let chats = Set(artifact.locations.filter { $0.workspace == .chat }.map(\.sessionID))
let images = Set(artifact.locations.filter { $0.workspace == .imageGeneration }.map(\.sessionID))
guard chats.allSatisfy({ !isActive(.chat, $0) }),
images.allSatisfy({ !isActive(.imageGeneration, $0) }) else { return false }
for id in chats.sorted(by: { $0.uuidString < $1.uuidString }) {
guard remove(.chat, id) else { return false }
}
for id in images.sorted(by: { $0.uuidString < $1.uuidString }) {
guard remove(.imageGeneration, id) else { return false }
}
return true
}
}
3 changes: 2 additions & 1 deletion Sources/Nativ/Features/Artifacts/ArtifactPreview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct ArtifactPreview: View {
let artifacts: [Artifact]
@Binding var selectedID: Artifact.ID?
let fileURL: (Artifact) -> URL
let displayName: (Artifact) -> String
let onClose: () -> Void
let onOpenChat: (Artifact) -> Void

Expand Down Expand Up @@ -56,7 +57,7 @@ struct ArtifactPreview: View {
.foregroundStyle(.white.opacity(0.7))
}
VStack(alignment: .leading, spacing: 2) {
Text(artifact.filename)
Text(displayName(artifact))
.legacyTextStyle(.compactCardTitle)
.foregroundStyle(.white)
.lineLimit(1)
Expand Down
Loading
Loading