import ComposableArchitecture import MarkdownUI import SharedUIComponents import SuggestionBasic import SwiftUI struct PromptToCodePanel: View { let store: StoreOf var body: some View { WithPerceptionTracking { VStack(spacing: 0) { TopBar(store: store) Content(store: store) .overlay(alignment: .bottom) { ActionBar(store: store) .padding(.bottom, 8) } Divider() Toolbar(store: store) } .background(.ultraThickMaterial) .xcodeStyleFrame() } } } extension PromptToCodePanel { struct TopBar: View { let store: StoreOf var body: some View { HStack { SelectionRangeButton(store: store) Spacer() CopyCodeButton(store: store) } .padding(2) } struct SelectionRangeButton: View { let store: StoreOf var body: some View { WithPerceptionTracking { Button(action: { store.send(.selectionRangeToggleTapped, animation: .linear(duration: 0.1)) }) { let attachedToFilename = store.filename let isAttached = store.isAttachedToSelectionRange let selectionRange = store.selectionRange let color: Color = isAttached ? .accentColor : .secondary.opacity(0.6) HStack(spacing: 4) { Image( systemName: isAttached ? "link" : "character.cursor.ibeam" ) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 14, height: 14) .frame(width: 20, height: 20, alignment: .center) .foregroundColor(.white) .background( color, in: RoundedRectangle( cornerRadius: 4, style: .continuous ) ) if isAttached { HStack(spacing: 4) { Text(attachedToFilename) .lineLimit(1) .truncationMode(.middle) if let range = selectionRange { Text(range.description) } }.foregroundColor(.primary) } else { Text("current selection").foregroundColor(.secondary) } } .padding(2) .padding(.trailing, 4) .overlay { RoundedRectangle(cornerRadius: 4, style: .continuous) .stroke(color, lineWidth: 1) } .background { RoundedRectangle(cornerRadius: 4, style: .continuous) .fill(color.opacity(0.2)) } .padding(2) } .keyboardShortcut("j", modifiers: [.command]) .buttonStyle(.plain) } } } struct CopyCodeButton: View { let store: StoreOf var body: some View { WithPerceptionTracking { if !store.code.isEmpty { CopyButton { store.send(.copyCodeButtonTapped) } } } } } } struct ActionBar: View { let store: StoreOf var body: some View { HStack { StopRespondingButton(store: store) ActionButtons(store: store) } } struct StopRespondingButton: View { let store: StoreOf var body: some View { WithPerceptionTracking { if store.isResponding { Button(action: { store.send(.stopRespondingButtonTapped) }) { HStack(spacing: 4) { Image(systemName: "stop.fill") Text("Stop") } .padding(8) .background( .regularMaterial, in: RoundedRectangle(cornerRadius: 6, style: .continuous) ) .overlay { RoundedRectangle(cornerRadius: 6, style: .continuous) .stroke(Color(nsColor: .separatorColor), lineWidth: 1) } } .buttonStyle(.plain) } } } } struct ActionButtons: View { @Perception.Bindable var store: StoreOf var body: some View { WithPerceptionTracking { let isResponding = store.isResponding let isCodeEmpty = store.code.isEmpty let isDescriptionEmpty = store.description.isEmpty var isRespondingButCodeIsReady: Bool { isResponding && !isCodeEmpty && !isDescriptionEmpty } if !isResponding || isRespondingButCodeIsReady { HStack { Toggle("Continuous Mode", isOn: $store.isContinuous) .toggleStyle(.checkbox) Button(action: { store.send(.cancelButtonTapped) }) { Text("Cancel") } .buttonStyle(CommandButtonStyle(color: .gray)) .keyboardShortcut("w", modifiers: [.command]) if !isCodeEmpty { Button(action: { store.send(.acceptButtonTapped) }) { Text("Accept(⌘ + ⏎)") } .buttonStyle(CommandButtonStyle(color: .accentColor)) .keyboardShortcut(KeyEquivalent.return, modifiers: [.command]) } } .padding(8) .background( .regularMaterial, in: RoundedRectangle(cornerRadius: 6, style: .continuous) ) .overlay { RoundedRectangle(cornerRadius: 6, style: .continuous) .stroke(Color(nsColor: .separatorColor), lineWidth: 1) } } } } } } struct Content: View { let store: StoreOf @Environment(\.colorScheme) var colorScheme @AppStorage(\.syncPromptToCodeHighlightTheme) var syncHighlightTheme @AppStorage(\.codeForegroundColorLight) var codeForegroundColorLight @AppStorage(\.codeForegroundColorDark) var codeForegroundColorDark @AppStorage(\.codeBackgroundColorLight) var codeBackgroundColorLight @AppStorage(\.codeBackgroundColorDark) var codeBackgroundColorDark var codeForegroundColor: Color? { if syncHighlightTheme { if colorScheme == .light, let color = codeForegroundColorLight.value?.swiftUIColor { return color } else if let color = codeForegroundColorDark.value?.swiftUIColor { return color } } return nil } var codeBackgroundColor: Color { if syncHighlightTheme { if colorScheme == .light, let color = codeBackgroundColorLight.value?.swiftUIColor { return color } else if let color = codeBackgroundColorDark.value?.swiftUIColor { return color } } return Color.contentBackground } var body: some View { WithPerceptionTracking { ScrollView { VStack(spacing: 0) { Spacer(minLength: 60) ErrorMessage(store: store) DescriptionContent(store: store, codeForegroundColor: codeForegroundColor) CodeContent(store: store, codeForegroundColor: codeForegroundColor) } } .background(codeBackgroundColor) .scaleEffect(x: 1, y: -1, anchor: .center) } } struct ErrorMessage: View { let store: StoreOf var body: some View { WithPerceptionTracking { if let errorMessage = store.error, !errorMessage.isEmpty { Text(errorMessage) .multilineTextAlignment(.leading) .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 4) .background( Color.red, in: RoundedRectangle(cornerRadius: 4, style: .continuous) ) .overlay { RoundedRectangle(cornerRadius: 4, style: .continuous) .stroke(Color.primary.opacity(0.2), lineWidth: 1) } .scaleEffect(x: 1, y: -1, anchor: .center) } } } } struct DescriptionContent: View { let store: StoreOf let codeForegroundColor: Color? var body: some View { WithPerceptionTracking { if !store.description.isEmpty { Markdown(store.description) .textSelection(.enabled) .markdownTheme(.gitHub.text { BackgroundColor(Color.clear) ForegroundColor(codeForegroundColor) }) .padding() .frame(maxWidth: .infinity) .scaleEffect(x: 1, y: -1, anchor: .center) } } } } struct CodeContent: View { let store: StoreOf let codeForegroundColor: Color? @AppStorage(\.wrapCodeInPromptToCode) var wrapCode var body: some View { WithPerceptionTracking { if store.code.isEmpty { Text( store.isResponding ? "Thinking..." : "Enter your requirement to generate code." ) .foregroundColor(codeForegroundColor?.opacity(0.7) ?? .secondary) .padding() .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .scaleEffect(x: 1, y: -1, anchor: .center) } else { if wrapCode { CodeBlockInContent( store: store, codeForegroundColor: codeForegroundColor ) } else { ScrollView(.horizontal) { CodeBlockInContent( store: store, codeForegroundColor: codeForegroundColor ) } .modify { if #available(macOS 13.0, *) { $0.scrollIndicators(.hidden) } else { $0 } } } } } } struct CodeBlockInContent: View { let store: StoreOf let codeForegroundColor: Color? @Environment(\.colorScheme) var colorScheme @AppStorage(\.promptToCodeCodeFont) var codeFont @AppStorage(\.hideCommonPrecedingSpacesInPromptToCode) var hideCommonPrecedingSpaces var body: some View { WithPerceptionTracking { let startLineIndex = store.selectionRange?.start.line ?? 0 let firstLinePrecedingSpaceCount = store.selectionRange?.start .character ?? 0 CodeBlock( code: store.code, language: store.language.rawValue, startLineIndex: startLineIndex, scenario: "promptToCode", colorScheme: colorScheme, firstLinePrecedingSpaceCount: firstLinePrecedingSpaceCount, font: codeFont.value.nsFont, droppingLeadingSpaces: hideCommonPrecedingSpaces, proposedForegroundColor: codeForegroundColor ) .frame(maxWidth: .infinity) .scaleEffect(x: 1, y: -1, anchor: .center) } } } } } struct Toolbar: View { let store: StoreOf @FocusState var focusField: PromptToCode.State.FocusField? struct RevertButtonState: Equatable { var isResponding: Bool var canRevert: Bool } var body: some View { HStack { RevertButton(store: store) HStack(spacing: 0) { InputField(store: store, focusField: $focusField) SendButton(store: store) } .frame(maxWidth: .infinity) .background { RoundedRectangle(cornerRadius: 6) .fill(Color(nsColor: .controlBackgroundColor)) } .overlay { RoundedRectangle(cornerRadius: 6) .stroke(Color(nsColor: .controlColor), lineWidth: 1) } .background { Button(action: { store.send(.appendNewLineToPromptButtonTapped) }) { EmptyView() } .keyboardShortcut(KeyEquivalent.return, modifiers: [.shift]) } .background { Button(action: { focusField = .textField }) { EmptyView() } .keyboardShortcut("l", modifiers: [.command]) } } .padding(8) .background(.ultraThickMaterial) } struct RevertButton: View { let store: StoreOf var body: some View { WithPerceptionTracking { Button(action: { store.send(.revertButtonTapped) }) { Group { Image(systemName: "arrow.uturn.backward") } .padding(6) .background { Circle().fill(Color(nsColor: .controlBackgroundColor)) } .overlay { Circle() .stroke(Color(nsColor: .controlColor), lineWidth: 1) } } .buttonStyle(.plain) .disabled(store.isResponding || !store.canRevert) } } } struct InputField: View { @Perception.Bindable var store: StoreOf var focusField: FocusState.Binding var body: some View { WithPerceptionTracking { AutoresizingCustomTextEditor( text: $store.prompt, font: .systemFont(ofSize: 14), isEditable: !store.isResponding, maxHeight: 400, onSubmit: { store.send(.modifyCodeButtonTapped) } ) .opacity(store.isResponding ? 0.5 : 1) .disabled(store.isResponding) .focused(focusField, equals: PromptToCode.State.FocusField.textField) .bind($store.focusedField, to: focusField) } .padding(8) .fixedSize(horizontal: false, vertical: true) } } struct SendButton: View { let store: StoreOf var body: some View { WithPerceptionTracking { Button(action: { store.send(.modifyCodeButtonTapped) }) { Image(systemName: "paperplane.fill") .padding(8) } .buttonStyle(.plain) .disabled(store.isResponding) .keyboardShortcut(KeyEquivalent.return, modifiers: []) } } } } } // MARK: - Previews #Preview("Default") { PromptToCodePanel(store: .init(initialState: .init( code: """ ForEach(0..