/ Jade / Views / PromptField.swift
PromptField.swift
  1  import SwiftUI
  2  
  3  struct PromptField: View {
  4      @Binding var prompt: String
  5      @State private var task: Task<Void, Never>?
  6      @FocusState private var isFocused: Bool
  7  
  8      let sendButtonAction: () async -> Void
  9      let mediaButtonAction: (() -> Void)?
 10  
 11      @State private var animatedColors: [Color] = [.blue, .purple, .blue]
 12  
 13      #if os(macOS)
 14      @State private var editorHeight: CGFloat = 60
 15      @State private var animate = false
 16      #endif
 17  
 18      var body: some View {
 19          VStack(spacing: 4) {
 20              #if os(macOS)
 21              HStack {
 22                  Spacer()
 23                  Rectangle()
 24                      .frame(width: 80, height: 6)
 25                      .foregroundColor(Color.gray.opacity(0.4))
 26                      .cornerRadius(3)
 27                      .padding(.vertical, 4)
 28                      .contentShape(Rectangle())
 29                      .onHover { _ in NSCursor.resizeUpDown.set() }
 30                      .gesture(
 31                          DragGesture(minimumDistance: 2)
 32                              .onChanged { value in
 33                                  editorHeight = max(40, editorHeight - value.translation.height)
 34                              }
 35                      )
 36                  Spacer()
 37              }
 38              #endif
 39  
 40              HStack(alignment: .bottom) {
 41                  if let mediaButtonAction {
 42                      Button(action: mediaButtonAction) {
 43                          Image(systemName: "photo.badge.plus")
 44                              .padding(.bottom, 4)
 45                      }
 46                  }
 47  
 48                  #if os(macOS)
 49                  ZStack {
 50                      if isRunning {
 51                          RoundedRectangle(cornerRadius: 6)
 52                              .stroke(
 53                                  AngularGradient(
 54                                      gradient: Gradient(colors: animatedColors),
 55                                      center: .center
 56                                  ),
 57                                  lineWidth: 2.5
 58                              )
 59                              .shadow(color: Color.purple.opacity(0.6), radius: 10)
 60                              .onAppear {
 61                                  startColorCycle()
 62                              }
 63                      } else {
 64                          RoundedRectangle(cornerRadius: 6)
 65                              .stroke(Color.gray.opacity(0.3), lineWidth: 1)
 66                      }
 67  
 68                      TextEditor(text: $prompt)
 69                          .focused($isFocused)
 70                          .font(.system(size: 14))
 71                          .padding(8)
 72                          .background(Color.clear)
 73                          .onReceive(NotificationCenter.default.publisher(for: .keyDownEvent)) { notif in
 74                              guard let event = notif.userInfo?["event"] as? NSEvent else { return }
 75                              if event.keyCode == 36 { // Return key
 76                                  if event.modifierFlags.contains(.shift) {
 77                                      prompt.append("\n")
 78                                  } else {
 79                                      let trimmed = prompt.trimmingCharacters(in: .whitespacesAndNewlines)
 80                                      prompt = trimmed
 81                                      task = Task {
 82                                          await sendButtonAction()
 83                                          removeTask()
 84                                      }
 85                                      prompt = ""
 86                                  }
 87                              }
 88                          }
 89                  }
 90                  .frame(height: editorHeight)
 91                  .background(Color(NSColor.textBackgroundColor))
 92                  .cornerRadius(6)
 93                  #else
 94                  ZStack {
 95                      if isRunning {
 96                          RoundedRectangle(cornerRadius: 8)
 97                              .stroke(
 98                                  AngularGradient(
 99                                      gradient: Gradient(colors: animatedColors),
100                                      center: .center
101                                  ),
102                                  lineWidth: 2.5
103                              )
104                              .shadow(color: Color.purple.opacity(0.6), radius: 10)
105                              .onAppear {
106                                  startColorCycle()
107                              }
108                      } else {
109                          RoundedRectangle(cornerRadius: 8)
110                              .stroke(Color.gray.opacity(0.3), lineWidth: 1)
111                      }
112  
113                      TextEditor(text: $prompt)
114                          .focused($isFocused)
115                          .padding(8)
116                          .font(.body)
117                          .background(Color(UIColor.secondarySystemBackground))
118                          .frame(minHeight: 20)
119                  }
120                  .frame(maxHeight: 100)
121                  #endif
122  
123                  Button {
124                      if isRunning {
125                          task?.cancel()
126                          removeTask()
127                      } else {
128                          task = Task {
129                              await sendButtonAction()
130                              removeTask()
131                          }
132                      }
133                  } label: {
134                      #if os(macOS)
135                      Label("Send", systemImage: isRunning ? "stop.circle.fill" : "paperplane.fill")
136                          .labelStyle(.titleAndIcon)
137                          .padding(.horizontal, 10)
138                          .padding(.vertical, 6)
139                          .background(Color.accentColor.opacity(0.1))
140                          .cornerRadius(8)
141                      #else
142                      Image(systemName: isRunning ? "stop.circle.fill" : "paperplane.fill")
143                          .font(.system(size: 20))
144                      #endif
145                  }
146                  .padding(.bottom, 4)
147              }
148          }
149      }
150  
151      private var isRunning: Bool {
152          task != nil && !(task!.isCancelled)
153      }
154  
155      private func removeTask() {
156          task = nil
157      }
158  
159      private func startColorCycle() {
160          Timer.scheduledTimer(withTimeInterval: 0.8, repeats: true) { _ in
161              guard isRunning else { return }
162              withAnimation(.easeInOut(duration: 0.8)) {
163                  let first = animatedColors.removeFirst()
164                  animatedColors.append(first)
165              }
166          }
167      }
168  }