kaizen-app/app/src/main/java/dev/kaizen/app/chat/ChatModels.kt
Bruno Deanoz 76ab8bfde9 feat: reasoning, sources, and tool-call display
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.
2026-06-21 15:35:46 +02:00

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