Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AIProject/iCo/Features/ChatBot/View/ChatInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,21 @@ struct ChatInputView: View {
Button {
Task { await viewModel.sendMessage(message: viewModel.searchText) }
} label: {
Image(systemName: "arrow.up")
Image(systemName: viewModel.isStreaming ? "square.fill" : "arrow.up")
.foregroundStyle(viewModel.isEditable && !viewModel.isStreaming ? .aiCoAccent : .aiCoNeutral)
.padding(10)
}
.frame(width: 30, height: 30)
.background {
Circle()
.fill(viewModel.isEditable ? .aiCoBackgroundAccent : .aiCoBackgroundWhite)
.fill(viewModel.isEditable && !viewModel.isStreaming ? .aiCoBackgroundAccent : .aiCoBackgroundWhite)
}
.onChange(of: viewModel.isTapped) {
isFocused = false
}
.overlay {
Circle()
.strokeBorder(viewModel.isEditable ? .accentGradient : .defaultGradient, lineWidth: 0.5)
.strokeBorder(viewModel.isEditable && !viewModel.isStreaming ? .accentGradient : .defaultGradient, lineWidth: 0.5)
}
.disabled(!viewModel.isEditable)
}
Expand Down
62 changes: 45 additions & 17 deletions AIProject/iCo/Features/ChatBot/ViewModel/ChatBotViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,46 +35,64 @@ final class ChatBotViewModel: ObservableObject {

/// 서버와 통신하는 클라이언트입니다.
private let chatBotClient: ChatBotClient
/// 메세지 전송 스트림 작업입니다.
private var streamTask: Task<Void, Error>?

init(chatBotClient: ChatBotClient = ChatBotClient()) {
self.chatBotClient = chatBotClient
}

/// 사용자가 입력한 메시지를 전송하고, 챗봇 응답 스트림을 관찰하여 UI에 반영합니다.
/// 만약 메세지가 이미 전송 중인 상태라면, 메세지 전송을 중단합니다.
/// - Parameter content: 사용자가 전송하는 메세지 내용입니다.
///
/// 이 메소드는 메인 쓰레드에서 실행됩니다.
@MainActor
func sendMessage(message: String) async {
searchText = ""
isStreaming = true

do {
if isStreaming {
cancelStream()
} else {
searchText = ""
isStreaming = true
addMessage(with: message)
isReceived = true
try await chatBotClient.connect(content: message)
try await observeStream()

do {
try await chatBotClient.connect(content: message)
try await observeStream()
} catch let error as NetworkError {
switch error {
case .taskCancelled:
chatBotClient.disconnect()
default:
chatBotClient.disconnect()
showStreamError()
}
} catch {
print("알 수 없는 에러 발생.")
}

isReceived = false
} catch {
await MainActor.run { showStreamError() }
isStreaming = false
}

isStreaming = false
}

/// 챗봇 SSE 스트림을 관찰하여 토큰 단위로 UI에 메시지를 업데이트합니다.
private func observeStream() async throws {
guard let stream = chatBotClient.stream else { return }

for try await content in stream {
try await Task.sleep(for: .seconds(0.05))
await MainActor.run {
if let index = messages.lastIndex(where: { !$0.isUser }) {
let message = messages[index]
messages[index] = ChatMessage(content: message.content + content, isUser: false)
streamTask = Task {
for try await content in stream {
await MainActor.run {
if let index = messages.lastIndex(where: { !$0.isUser }) {
let message = messages[index]
messages[index] = ChatMessage(content: message.content + content, isUser: false)
}
}
}
}

try await streamTask?.value
}

/// 사용자 메시지와 빈 챗봇 응답 메시지를 목록에 추가합니다.
Expand All @@ -92,7 +110,7 @@ final class ChatBotViewModel: ObservableObject {
/// 이 메소드는 메인 쓰레드에서 실행됩니다.
@MainActor
private func checkValid() {
isEditable = !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isStreaming
isEditable = !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || isStreaming
}

/// SSE 데이터 전달 과정에서 에러가 발생했을 때 호출합니다.
Expand All @@ -104,4 +122,14 @@ final class ChatBotViewModel: ObservableObject {
messages[index] = ChatMessage(content: "알 수 없는 에러가 발생했습니다.", isUser: false, isError: true)
}
}

/// 메세지 전송을 취소합니다.
///
/// 이 메소드는 메인 쓰레드에서 실행됩니다.
@MainActor
private func cancelStream() {
chatBotClient.continuation?.finish(throwing: NetworkError.taskCancelled)
streamTask?.cancel()
streamTask = nil
}
}
Loading