// // ChatView.swift // oAI // // Main chat interface // import SwiftUI struct ChatView: View { @Environment(ChatViewModel.self) var viewModel let onModelSelect: () -> Void let onProviderChange: (Settings.Provider) -> Void var body: some View { @Bindable var viewModel = viewModel VStack(spacing: 0) { // Header HeaderView( provider: viewModel.currentProvider, model: viewModel.selectedModel, stats: viewModel.sessionStats, onlineMode: viewModel.onlineMode, mcpEnabled: viewModel.mcpEnabled, mcpStatus: viewModel.mcpStatus, onModelSelect: onModelSelect, onProviderChange: onProviderChange ) // Messages ScrollViewReader { proxy in ScrollView { LazyVStack(alignment: .leading, spacing: 12) { ForEach(viewModel.messages) { message in MessageRow(message: message, viewModel: viewModel) .id(message.id) } // Processing indicator if viewModel.isGenerating && viewModel.messages.last?.isStreaming != true { ProcessingIndicator() .padding(.horizontal) } // Invisible bottom anchor for auto-scroll Color.clear .frame(height: 1) .id("bottom") } .padding() } .background(Color.oaiBackground) .onChange(of: viewModel.messages.count) { withAnimation { proxy.scrollTo("bottom", anchor: .bottom) } } .onChange(of: viewModel.messages.last?.content) { // Auto-scroll as streaming content arrives if viewModel.isGenerating { proxy.scrollTo("bottom", anchor: .bottom) } } } // Auto-continue countdown banner if viewModel.isAutoContinuing { HStack(spacing: 12) { ProgressView() .progressViewStyle(.circular) .scaleEffect(0.7) VStack(alignment: .leading, spacing: 2) { Text(ThinkingVerbs.random()) .font(.system(size: 13, weight: .medium)) .foregroundColor(.oaiPrimary) Text("Continuing in \(viewModel.autoContinueCountdown)s") .font(.system(size: 11)) .foregroundColor(.oaiSecondary) } Spacer() Button("Cancel") { viewModel.cancelAutoContinue() } .buttonStyle(.bordered) .controlSize(.small) .tint(.red) } .padding(.horizontal, 16) .padding(.vertical, 10) .background(Color.blue.opacity(0.1)) .overlay( Rectangle() .stroke(Color.blue.opacity(0.3), lineWidth: 1) ) } // Input bar InputBar( text: $viewModel.inputText, isGenerating: viewModel.isGenerating, mcpStatus: viewModel.mcpStatus, onlineMode: viewModel.onlineMode, onSend: viewModel.sendMessage, onCancel: viewModel.cancelGeneration ) // Footer FooterView(stats: viewModel.sessionStats) } .background(Color.oaiBackground) .sheet(isPresented: $viewModel.showShortcuts) { ShortcutsView() } .sheet(isPresented: $viewModel.showSkills) { AgentSkillsView() } } } struct ProcessingIndicator: View { @State private var animating = false @State private var thinkingText = ThinkingVerbs.random() var body: some View { HStack(spacing: 8) { Text(thinkingText) .font(.system(size: 14, weight: .medium)) .foregroundColor(.oaiSecondary) HStack(spacing: 4) { ForEach(0..<3) { index in Circle() .fill(Color.oaiSecondary) .frame(width: 6, height: 6) .scaleEffect(animating ? 1.0 : 0.5) .animation( .easeInOut(duration: 0.6) .repeatForever() .delay(Double(index) * 0.2), value: animating ) } } } .padding(.horizontal, 16) .padding(.vertical, 12) .background(Color.oaiSecondary.opacity(0.05)) .cornerRadius(8) .onAppear { animating = true } } } #Preview { ChatView(onModelSelect: {}, onProviderChange: { _ in }) .environment(ChatViewModel()) }