StreamConsumer.Incremental now returns StreamState (content, reasoning, tools, query, sources) instead of just visible text. All sentinel sections are parsed and structured: - \x01 REASONING: captured as separate string - \x02 SOURCES: parsed as List<SearchSource> (title, url, snippet) - \x03 TOOL: parsed as List<ToolEvent> (type, name, args, result, elapsed) - \x04 QUERY: captured as search query string UI components: - ReasoningBlock: collapsible "Gedankengang" with expand/collapse animation - ToolEventsBlock: shows tool-call steps with status icons (start/end/error) - SourcesBlock: web search sources as compact cards with domain extraction Message model extended with reasoning, tools, query, sources fields. KaizenApi.chat() returns Flow<StreamState> instead of Flow<String>. ChatScreen wires all fields to Message during streaming. Fixed: empty append() on Incremental no longer prematurely ends tool phase. StreamConsumerTest rewritten to verify all parsed sections.
61 lines
2.4 KiB
Kotlin
61 lines
2.4 KiB
Kotlin
package dev.kaizen.app.chat
|
|
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.rounded.Adjust
|
|
import androidx.compose.material.icons.rounded.AutoAwesome
|
|
import androidx.compose.material.icons.rounded.Image
|
|
import androidx.compose.material.icons.rounded.Language
|
|
import androidx.compose.material.icons.rounded.Movie
|
|
import androidx.compose.material.icons.rounded.MusicNote
|
|
import androidx.compose.material.icons.rounded.Tune
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.ui.graphics.vector.ImageVector
|
|
import androidx.compose.ui.res.stringResource
|
|
import dev.kaizen.app.R
|
|
import dev.kaizen.app.net.Attachment
|
|
import dev.kaizen.app.net.SearchSource
|
|
import dev.kaizen.app.net.ToolEvent
|
|
|
|
enum class Role { User, Assistant }
|
|
|
|
data class Message(
|
|
val id: Long,
|
|
val role: Role,
|
|
val content: String,
|
|
val streaming: Boolean = false,
|
|
val thinking: Boolean = false,
|
|
val wireId: String = java.util.UUID.randomUUID().toString(),
|
|
val attachments: List<Attachment> = emptyList(),
|
|
val reasoning: String = "",
|
|
val tools: List<ToolEvent> = emptyList(),
|
|
val query: String = "",
|
|
val sources: List<SearchSource> = emptyList(),
|
|
)
|
|
|
|
data class Suggestion(val labelRes: Int, val promptRes: Int, val icon: ImageVector)
|
|
|
|
val suggestions = listOf(
|
|
Suggestion(R.string.suggest_brainstorm, R.string.suggest_brainstorm_prompt, Icons.Rounded.AutoAwesome),
|
|
Suggestion(R.string.suggest_image, R.string.suggest_image_prompt, Icons.Rounded.Image),
|
|
Suggestion(R.string.suggest_video, R.string.suggest_video_prompt, Icons.Rounded.Movie),
|
|
Suggestion(R.string.suggest_web, R.string.suggest_web_prompt, Icons.Rounded.Language),
|
|
)
|
|
|
|
data class ChatMode(val labelRes: Int, val icon: ImageVector)
|
|
|
|
val chatModes = listOf(
|
|
ChatMode(R.string.mode_standard, Icons.Rounded.Adjust),
|
|
ChatMode(R.string.mode_sampling, Icons.Rounded.Tune),
|
|
ChatMode(R.string.mode_image, Icons.Rounded.Image),
|
|
ChatMode(R.string.mode_search, Icons.Rounded.Language),
|
|
ChatMode(R.string.mode_video, Icons.Rounded.Movie),
|
|
ChatMode(R.string.mode_audio, Icons.Rounded.MusicNote),
|
|
)
|
|
|
|
@Composable
|
|
fun greeting(hour: Int): String = when (hour) {
|
|
in 5..11 -> stringResource(R.string.greeting_morning)
|
|
in 12..17 -> stringResource(R.string.greeting_day)
|
|
in 18..22 -> stringResource(R.string.greeting_evening)
|
|
else -> stringResource(R.string.greeting_night)
|
|
}
|