Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.sse.SSE
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
Expand All @@ -30,6 +31,7 @@ internal fun HttpClientConfig<*>.installTodakunDefaults(
url(baseUrl)
contentType(ContentType.Application.Json)
}
install(SSE)
expectSuccess = true
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.kikidan.data_remote.dto.chat

import kotlinx.serialization.Serializable

@Serializable
data class ChatEntryResponse(
val greeting: String,
val suggestions: List<ChatSuggestionResponse> = emptyList(),
val quota: ChatQuotaResponse,
)

@Serializable
data class ChatSuggestionResponse(
val emoji: String,
val label: String,
val seedPrompt: String,
val category: String,
)

@Serializable
data class ChatQuotaResponse(
val used: Int,
val limit: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.kikidan.data_remote.dto.chat

import kotlinx.serialization.Serializable

@Serializable
data class ChatMessageResponse(
val id: String,
val role: String,
val content: String,
val status: String,
val action: ChatActionResponse? = null,
val createdAt: String,
)

@Serializable
data class ChatActionResponse(
val type: String,
val label: String,
val category: String,
val date: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.kikidan.data_remote.dto.chat

import kotlinx.serialization.Serializable

// SSE 이벤트 data payload. 백엔드 이벤트 DTO와 1:1이며 전 필드 non-null이다.

@Serializable
data class ChatStreamStartResponse(
val conversationId: String,
val userMessageId: String,
val assistantMessageId: String,
val quotaUsed: Int,
val quotaLimit: Int,
)

@Serializable
data class ChatStreamDeltaResponse(
val text: String,
)

@Serializable
data class ChatStreamDoneResponse(
val assistantMessageId: String,
)

@Serializable
data class ChatStreamErrorResponse(
val code: String,
val message: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.kikidan.data_remote.dto.chat

import kotlinx.serialization.Serializable

@Serializable
data class ConversationDetailResponse(
val id: String,
val title: String,
val messages: List<ChatMessageResponse> = emptyList(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.kikidan.data_remote.dto.chat

import kotlinx.serialization.Serializable

@Serializable
data class ConversationListResponse(
val conversations: List<ConversationSummaryResponse> = emptyList(),
)

@Serializable
data class ConversationSummaryResponse(
val id: String,
val title: String,
val lastMessageAt: String,
val unread: Boolean = false,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.kikidan.data_remote.dto.chat

import kotlinx.serialization.Serializable

@Serializable
data class SendChatMessageRequest(
val conversationId: String? = null,
val content: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.kikidan.data_remote.sse

import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpTimeoutConfig
import io.ktor.client.plugins.sse.sse
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.sse.ServerSentEvent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

// 이벤트 간 무응답 상한. 무한으로 두면 서버가 조용히 죽었을 때 앱이 영원히 대기한다.
private const val SSE_SOCKET_TIMEOUT_MS = 60_000L

/**
* SSE 엔드포인트를 열어 서버 이벤트를 Flow로 방출한다.
* collect가 취소되면 연결도 닫힌다(sse()의 세션 수명이 곧 이 Flow의 수명). 비-2xx 응답은 collect 시점에 예외로 전파된다.
*/
internal fun HttpClient.serverSentEvents(
urlString: String,
block: HttpRequestBuilder.() -> Unit = {},
): Flow<ServerSentEvent> =
flow {
sse(
urlString = urlString,
request = {
block()
// 전역 HttpTimeout(requestTimeoutMillis=15s)이 장시간 스트림을 끊으므로 반드시 덮어쓴다.
// SSE 전체 길이 상한은 없애고, 소켓 무응답만 60s로 감지한다.
timeout {
requestTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
socketTimeoutMillis = SSE_SOCKET_TIMEOUT_MS
}
},
) {
incoming.collect { event -> emit(event) }
}
}
Loading