Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftUI
import UIKit

/// A system presentation prepared by an artifact file action.
public enum ChatArtifactFileActionPresentation: Identifiable, Equatable {
public enum ChatArtifactFileActionPresentation: Identifiable, Equatable, Sendable {
/// Presents the standard activity controller for a local file.
case share(URL)
/// Presents the Files document picker in export-as-copy mode.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#if os(iOS)
import SwiftUI
import UIKit

struct ChatArtifactViewerActionsMenu: View, Equatable {
let value: ChatArtifactViewerActionsMenuValue
let actions: ChatArtifactViewerActionsMenuActions

nonisolated static func == (lhs: Self, rhs: Self) -> Bool {
lhs.value == rhs.value
}

private var snapshot: ChatArtifactViewerPageSnapshot { value.snapshot }

var body: some View {
Menu {
if snapshot.hasFileActions {
Section {
fileActionButtons
}
}
if snapshot.shouldShowTextJumpControls {
Section {
textViewerActionButtons
}
}
if snapshot.state == .markdown,
snapshot.markdownPresentation.isRenderedAvailable {
Section {
Picker(
String(
localized: "chat.artifact.markdown.view",
defaultValue: "Markdown view",
bundle: .module
),
selection: Binding(
get: { snapshot.markdownPresentation.mode },
set: { actions.selectMarkdownMode(snapshot.path, $0) }
)
) {
Text(String(
localized: "chat.artifact.markdown.raw",
defaultValue: "Raw",
bundle: .module
))
.tag(ChatArtifactMarkdownMode.raw)
Text(String(
localized: "chat.artifact.markdown.rendered",
defaultValue: "Rendered",
bundle: .module
))
.tag(ChatArtifactMarkdownMode.rendered)
}
}
}
} label: {
Label(
String(
localized: "chat.artifact.viewer.actions",
defaultValue: "Viewer actions",
bundle: .module
),
systemImage: "ellipsis.circle"
)
}
.disabled(snapshot.fileActionState.isRunning)
}

@ViewBuilder
private var fileActionButtons: some View {
Button {
actions.prepareShare(snapshot.path)
} label: {
Label(
String(localized: "chat.artifact.share", defaultValue: "Share", bundle: .module),
systemImage: "square.and.arrow.up"
)
}
Button {
actions.prepareSave(snapshot.path)
} label: {
Label(
String(localized: "chat.artifact.save_to_files", defaultValue: "Save to Files", bundle: .module),
systemImage: "folder.badge.plus"
)
}
if snapshot.isTextFile {
Button {
UIPasteboard.general.string = snapshot.renderedText
actions.notifyCopied()
} label: {
Label(
String(localized: "chat.artifact.copy_contents", defaultValue: "Copy contents", bundle: .module),
systemImage: "doc.on.doc"
)
}
.disabled(!snapshot.canCopyContents)
}
Button {
UIPasteboard.general.string = snapshot.path
actions.notifyPathCopied()
} label: {
Label(
String(localized: "chat.artifact.copy_path", defaultValue: "Copy path", bundle: .module),
systemImage: "link"
)
}
}

@ViewBuilder
private var textViewerActionButtons: some View {
Button {
withAnimation(.snappy) {
actions.toggleSearch(snapshot.path)
}
} label: {
Label(
String(
localized: "chat.artifact.search.title",
defaultValue: "Search",
bundle: .module
),
systemImage: "magnifyingglass"
)
}
Button {
withAnimation(.snappy) {
actions.toggleGoToLine(snapshot.path)
}
} label: {
Label(
String(
localized: "chat.artifact.line.goto",
defaultValue: "Go to line",
bundle: .module
),
systemImage: "text.line.first.and.arrowtriangle.forward"
)
}
Button {
actions.requestTop(snapshot.path)
} label: {
Label(
String(
localized: "chat.artifact.jump.top",
defaultValue: "Top",
bundle: .module
),
systemImage: "arrow.up.to.line"
)
}
Button {
actions.requestBottom(snapshot.path)
} label: {
Label(jumpToEndTitle, systemImage: "arrow.down.to.line")
}
Button {
actions.toggleLineNumbers(snapshot.path)
} label: {
Label(
String(
localized: "chat.artifact.line.numbers",
defaultValue: "Line numbers",
bundle: .module
),
systemImage: snapshot.showsLineNumbers ? "checkmark" : "number"
)
}
Button {
actions.toggleWordWrap(snapshot.path)
} label: {
Label(
String(
localized: "chat.artifact.wrap",
defaultValue: "Word wrap",
bundle: .module
),
systemImage: snapshot.wrapsLines ? "checkmark" : "text.justify.left"
)
}
}

private var jumpToEndTitle: String {
switch ChatArtifactTextEndJumpTarget(reachedEOF: snapshot.textReachedEOF) {
case .end:
return String(
localized: "chat.artifact.jump.end",
defaultValue: "End",
bundle: .module
)
case .latest:
return String(
localized: "chat.artifact.jump.latest",
defaultValue: "Latest",
bundle: .module
)
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#if os(iOS)
struct ChatArtifactViewerActionsMenuActions {
let prepareShare: (String) -> Void
let prepareSave: (String) -> Void
let toggleSearch: (String) -> Void
let toggleGoToLine: (String) -> Void
let requestTop: (String) -> Void
let requestBottom: (String) -> Void
let toggleLineNumbers: (String) -> Void
let toggleWordWrap: (String) -> Void
let selectMarkdownMode: (String, ChatArtifactMarkdownMode) -> Void
let notifyCopied: () -> Void
let notifyPathCopied: () -> Void
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#if os(iOS)
struct ChatArtifactViewerActionsMenuValue: Equatable, Sendable {
let snapshot: ChatArtifactViewerPageSnapshot
let loaderScope: ChatArtifactLoaderScope
let loaderSupportsArtifacts: Bool
let loaderSupportsDirectoryBrowsing: Bool
Comment thread
azooz2003-bit marked this conversation as resolved.
}
#endif
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// Immutable system-presentation state owned by one artifact page.
struct ChatArtifactViewerFileActionState: Equatable {
struct ChatArtifactViewerFileActionState: Equatable, Sendable {
#if os(iOS)
var presentation: ChatArtifactFileActionPresentation? = nil
#endif
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// Immutable render state for one path-keyed artifact viewer page.
struct ChatArtifactViewerPageSnapshot: Identifiable, Equatable {
struct ChatArtifactViewerPageSnapshot: Identifiable, Equatable, Sendable {
let path: String
let state: ChatArtifactViewerState
let textChunks: [String]
Expand Down
Loading