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

178 lines
6.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}