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 }