montana/Русский/Сайт/MontanaApp/Montana/Sources/Views/ChatView.swift

178 lines
6.2 KiB
Swift
Raw Normal View History

import SwiftUI
struct ChatView: View {
@EnvironmentObject var appState: AppState
@State private var messages: [ChatMessage] = []
@State private var inputText = ""
@State private var isLoading = false
@FocusState private var isInputFocused: Bool
var body: some View {
NavigationView {
VStack(spacing: 0) {
// Messages
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 12) {
ForEach(messages) { message in
MessageBubble(message: message)
.id(message.id)
}
if isLoading {
HStack {
TypingIndicator()
Spacer()
}
.padding(.horizontal)
}
}
.padding()
}
.onChange(of: messages.count) { _ in
if let last = messages.last {
withAnimation {
proxy.scrollTo(last.id, anchor: .bottom)
}
}
}
}
// Input
HStack(spacing: 12) {
TextField("Сообщение...", text: $inputText)
.padding(12)
.background(Color("Card"))
.cornerRadius(20)
.focused($isInputFocused)
Button(action: sendMessage) {
Image(systemName: "arrow.up")
.fontWeight(.semibold)
.foregroundColor(Color("Background"))
.frame(width: 40, height: 40)
.background(Color("Gold"))
.clipShape(Circle())
}
.disabled(inputText.trimmingCharacters(in: .whitespaces).isEmpty || isLoading)
}
.padding()
.background(Color("Background"))
}
.background(Color("Background").ignoresSafeArea())
.navigationTitle("Юнона ☀️")
.onAppear {
if messages.isEmpty {
// Welcome message
messages.append(ChatMessage(
role: "assistant",
content: "Привет! Я Юнона ☀️\n\nЯ AI ассистент Montana Protocol. Могу помочь с переводами, рассказать о протоколе или просто поговорить.\n\nО чём хочешь поговорить?"
))
}
}
}
}
private func sendMessage() {
let text = inputText.trimmingCharacters(in: .whitespaces)
guard !text.isEmpty else { return }
// Add user message
messages.append(ChatMessage(role: "user", content: text))
inputText = ""
isLoading = true
// Send to API
guard let deviceId = UserDefaults.standard.string(forKey: "deviceId") else {
messages.append(ChatMessage(role: "assistant", content: "Ошибка: не авторизован"))
isLoading = false
return
}
API.shared.sendMessage(deviceId: deviceId, message: text) { result in
DispatchQueue.main.async {
isLoading = false
switch result {
case .success(let response):
messages.append(ChatMessage(role: "assistant", content: response))
case .failure:
messages.append(ChatMessage(role: "assistant", content: "Ошибка соединения. Попробуй позже."))
}
}
}
}
}
// MARK: - Message Bubble
struct MessageBubble: View {
let message: ChatMessage
var isUser: Bool { message.role == "user" }
var body: some View {
HStack {
if isUser { Spacer() }
Text(message.content)
.padding(12)
.background(
isUser ?
LinearGradient(colors: [Color("Gold"), Color(hex: "FFA500")], startPoint: .leading, endPoint: .trailing) :
LinearGradient(colors: [Color("Card"), Color("Card")], startPoint: .leading, endPoint: .trailing)
)
.foregroundColor(isUser ? Color("Background") : .white)
.cornerRadius(18)
.cornerRadius(isUser ? 4 : 18, corners: isUser ? .bottomRight : .bottomLeft)
if !isUser { Spacer() }
}
}
}
// MARK: - Typing Indicator
struct TypingIndicator: View {
@State private var animating = false
var body: some View {
HStack(spacing: 4) {
ForEach(0..<3) { i in
Circle()
.fill(Color.secondary)
.frame(width: 8, height: 8)
.scaleEffect(animating ? 1 : 0.5)
.animation(
.easeInOut(duration: 0.6)
.repeatForever()
.delay(Double(i) * 0.2),
value: animating
)
}
}
.padding(12)
.background(Color("Card"))
.cornerRadius(18)
.onAppear { animating = true }
}
}
// MARK: - Corner Radius Extension
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}