diff --git a/package-kuring/Sources/Caches/Dependency/Bots.swift b/package-kuring/Sources/Caches/Dependency/Bots.swift index 0b73273..69c0569 100644 --- a/package-kuring/Sources/Caches/Dependency/Bots.swift +++ b/package-kuring/Sources/Caches/Dependency/Bots.swift @@ -7,16 +7,15 @@ import SwiftData import Dependencies import Models import SwiftUI -import Foundation public struct BotDataBase { public var fetch: @Sendable (FetchDescriptor) throws -> [ChatInfo] public var add: @Sendable (ChatInfo) throws -> Void - public var update: @Sendable (ChatInfo) throws -> Void + public var delete: @Sendable (ChatInfo) throws -> Void public enum BotError: Error { case add - case update + case delete } } @@ -28,21 +27,24 @@ extension BotDataBase: DependencyKey { return try botContext.fetch(descriptor) }, add: { model in - @Dependency(\.swiftData.context) var modelContext - let botContext = try modelContext() - botContext.insert(model) - try botContext.save() - }, - update: { model in - @Dependency(\.swiftData.context) var modelContext - let botContext = try modelContext() - let modelID = model.index - /// index 일치 확인 - if let existing = try botContext.fetch(FetchDescriptor(predicate: #Predicate { $0.index == modelID })).first { - existing.text = model.text + do { + @Dependency(\.swiftData.context) var modelContext + let botContext = try modelContext() + botContext.insert(model) try botContext.save() - } else { - throw BotError.update + } catch { + throw BotError.add + } + }, + delete: { model in + do { + @Dependency(\.swiftData.context) var modelContext + let botContext = try modelContext() + + let modelToBeDelete = model + botContext.delete(modelToBeDelete) + } catch { + throw BotError.delete } } ) @@ -54,13 +56,13 @@ extension BotDataBase: TestDependencyKey { public static let testValue = Self( fetch: unimplemented("\(Self.self).fetchDescriptor"), add: unimplemented("\(Self.self).add"), - update: unimplemented("\(Self.self).update") + delete: unimplemented("\(Self.self).delete") ) public static let noop = Self( fetch: { _ in [] }, add: { _ in }, - update: { _ in } + delete: { _ in } ) } diff --git a/package-kuring/Sources/Features/BotFeatures/BotFeature.swift b/package-kuring/Sources/Features/BotFeatures/BotFeature.swift index 627ba68..4e5e56b 100644 --- a/package-kuring/Sources/Features/BotFeatures/BotFeature.swift +++ b/package-kuring/Sources/Features/BotFeatures/BotFeature.swift @@ -3,7 +3,6 @@ // See the 'License.txt' file for licensing information. // -import Foundation import ComposableArchitecture import Networks import SwiftData @@ -18,15 +17,18 @@ public struct BotFeature { public var chatInfo: ChatInfo = .init() public var chatHistory: [ChatInfo] = [] public var focus: Field? = .question + public var isLoading: Bool = false public init( chatInfo: ChatInfo = .init(), chatHistory: [ChatInfo] = [], - focus: Field? = .question + focus: Field? = .question, + isLoading: Bool = false ){ self.chatInfo = chatInfo self.chatHistory = chatHistory self.focus = focus + self.isLoading = isLoading } var fetchDescriptor: FetchDescriptor { @@ -46,10 +48,10 @@ public struct BotFeature { } public enum Action: BindableAction, Equatable { + case binding(BindingAction) case sendMessage - case addQuestion(String) case messageResponse(Result) - case binding(BindingAction) + case addQuestion(String) case queryChanged([ChatInfo]) case onAppear @@ -76,8 +78,7 @@ public struct BotFeature { case .sendMessage: state.focus = nil - state.chatInfo.chatStatus = .waiting - + state.isLoading = true return .run { [question = state.chatInfo.text] send in do { SSEClient.shared.sessionStart(question: question) @@ -107,34 +108,38 @@ public struct BotFeature { } case let .messageResponse(.success(message)): + state.isLoading = false if let lastMessage = state.chatHistory.last, lastMessage.type == .answer { state.chatHistory[state.chatHistory.count - 1].text += message - } else { - let newResponse = ChatInfo( - index: state.chatHistory.count + 1, - text: message, - type: .answer, - chatStatus: .complete - ) - state.chatHistory.append(newResponse) - do { try context.add(newResponse) } catch {} + state.chatHistory[state.chatHistory.count - 1].chatStatus = .complete } return .none case let .messageResponse(.failure(error)): - state.chatInfo.chatStatus = .failure print(error.localizedDescription) return .none case let .addQuestion(question): let newQuestion = ChatInfo( - index: state.chatHistory.count + 1, + index: state.chatHistory.count, text: question, type: .question, chatStatus: .complete ) - do { try context.add(newQuestion) } catch {} + state.chatHistory.append(newQuestion) + do { try context.add(newQuestion) } catch {} + + let newResponse = ChatInfo( + index: state.chatHistory.count, + text: "", + type: .answer, + chatStatus: .waiting + ) + + state.chatHistory.append(newResponse) + do { try context.add(newResponse) } catch {} + return .none case .queryChanged(let newMessage): diff --git a/package-kuring/Sources/UIKit/BotUI/BotView.swift b/package-kuring/Sources/UIKit/BotUI/BotView.swift index 93e8847..9917153 100644 --- a/package-kuring/Sources/UIKit/BotUI/BotView.swift +++ b/package-kuring/Sources/UIKit/BotUI/BotView.swift @@ -7,9 +7,7 @@ import SwiftUI import ComposableArchitecture import ColorSet import Networks -import Models import SwiftData -import Caches import BotFeatures public struct BotView: View { @@ -18,6 +16,7 @@ public struct BotView: View { @State private var isPopoverVisible = false @State private var isSendPopupVisible = false @State private var tempInputText: String = "" + @State private var isLoading = false public var body: some View { ZStack { @@ -118,7 +117,7 @@ public struct BotView: View { sendButton } .padding(.horizontal, 20) - .disabled(store.chatHistory.count == 4) +// .disabled(store.chatHistory.count >= 4) } private var sendButton: some View { @@ -132,7 +131,7 @@ public struct BotView: View { .scaledToFit() .frame(width: 40, height: 40) } - .disabled(store.chatHistory.count == 4) +// .disabled(store.chatHistory.count >= 4) } private var infoText: some View { @@ -144,9 +143,11 @@ public struct BotView: View { private var sendPopup: some View { SendPopup(isVisible: $isSendPopupVisible) { + isLoading = true store.send(.addQuestion(tempInputText)) tempInputText = "" store.send(.sendMessage) + isLoading = false } } diff --git a/package-kuring/Sources/UIKit/BotUI/ChatView.swift b/package-kuring/Sources/UIKit/BotUI/ChatView.swift index e783c53..af897c7 100644 --- a/package-kuring/Sources/UIKit/BotUI/ChatView.swift +++ b/package-kuring/Sources/UIKit/BotUI/ChatView.swift @@ -9,7 +9,6 @@ import ComposableArchitecture import Lottie import BotFeatures import Models -import Caches import SwiftData struct ChatView: View { @@ -20,42 +19,43 @@ struct ChatView: View { ScrollView { VStack(alignment: .center, spacing: 16) { ForEach(store.chatHistory) { chat in - HStack(alignment: .top) { - if chat.type == .question { - Spacer() - messageBubble(for: chat) - userImage(for: chat.type) - } else { - userImage(for: chat.type) - chatStatusView(for: chat) - Spacer() - } - } - .padding(chat.type == .question ? .trailing : .leading, 16) + chatRow(for: chat) if chat.type == .answer { - possibleCountText(for: 2 - (chat.index / 2)) + /// 질문 가능 횟수 + possibleCountText(for: 2 - (chat.index / 2) + 1) } } Spacer() } .padding(.bottom, 5) - } - /// query 변경 감지 - .onChange(of: self.chatQuery, initial: true) {_, newValue in + .onChange(of: self.chatQuery, initial: true) { _, newValue in store.send(.queryChanged(newValue)) } } - + @ViewBuilder - private func chatStatusView(for message: ChatInfo) -> some View { - switch message.chatStatus { - case .waiting: - lottieView - case .complete: - messageBubble(for: message) - default: + private func chatRow(for chat: ChatInfo) -> some View { + HStack(alignment: .top) { + if chat.type == .question { + Spacer() + messageBubble(for: chat) + userImage(for: chat.type) + } else { + userImage(for: chat.type) + responseContentView(for: chat) + Spacer() + } + } + .padding(chat.type == .question ? .trailing : .leading, 16) + } + + @ViewBuilder + private func responseContentView(for chat: ChatInfo) -> some View { + if chat.index == store.chatHistory.count - 1, store.state.isLoading { lottieView + } else { + messageBubble(for: chat) } } @@ -63,8 +63,7 @@ struct ChatView: View { LottieView(animation: .named("animation_loading.json", bundle: Bundle.bots)) .playing() .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) + .frame(width: 70, alignment: .leading) } private func messageBubble(for message: ChatInfo) -> some View { @@ -93,13 +92,13 @@ struct ChatView: View { } private func possibleCountText(for sendCount: Int) -> some View { - let currentDate = formattedCurrentDate() + let currentDate = formattedCurrentDate return Text("질문 가능 횟수 \(sendCount)회 (\(currentDate) 기준)") .font(.system(size: 12, weight: .medium)) .foregroundStyle(sendCount == 0 ? Color.Kuring.warning : Color.Kuring.caption1) } - private func formattedCurrentDate() -> String { + private var formattedCurrentDate: String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy.MM.dd" return dateFormatter.string(from: Date())