Further improve the chat page
This commit is contained in:
parent
aca314d218
commit
a2597878d1
15 changed files with 550 additions and 248 deletions
|
|
@ -57,6 +57,7 @@ dependencies {
|
||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
implementation(libs.retrofit)
|
implementation(libs.retrofit)
|
||||||
implementation(libs.retrofit.converter.gson)
|
implementation(libs.retrofit.converter.gson)
|
||||||
|
implementation(libs.androidx.material3.window.size.class1)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ChatApp" >
|
android:theme="@style/Theme.ChatApp"
|
||||||
|
android:enableOnBackInvokedCallback="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.chat.ChatActivity"
|
android:name=".activity.chat.ChatActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/title_activity_chat"
|
android:label="@string/title_activity_chat"
|
||||||
android:theme="@style/Theme.ChatApp" >
|
android:theme="@style/Theme.ChatApp"
|
||||||
|
android:windowSoftInputMode="adjustNothing">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -20,6 +22,7 @@ import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
|
@ -31,132 +34,362 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||||
|
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||||
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import eu.m724.chatapp.activity.chat.compose.AnimatedChangingText
|
import eu.m724.chatapp.activity.chat.ChatState.Companion.rememberChatState
|
||||||
import eu.m724.chatapp.activity.chat.compose.LanguageModelMistakeWarning
|
import eu.m724.chatapp.activity.chat.composable.AnimatedChangingText
|
||||||
|
import eu.m724.chatapp.activity.chat.composable.LanguageModelMistakeWarning
|
||||||
|
import eu.m724.chatapp.activity.chat.composable.NestedScrollKeyboardHider
|
||||||
|
import eu.m724.chatapp.activity.chat.composable.SimpleTextFieldWithPadding
|
||||||
|
import eu.m724.chatapp.activity.chat.composable.disableBringIntoViewOnFocus
|
||||||
import eu.m724.chatapp.activity.ui.theme.ChatAppTheme
|
import eu.m724.chatapp.activity.ui.theme.ChatAppTheme
|
||||||
|
import eu.m724.chatapp.api.data.ChatMessage
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ChatActivity : ComponentActivity() {
|
class ChatActivity : ComponentActivity() {
|
||||||
private val viewModel: ChatActivityViewModel by viewModels()
|
private val viewModel: ChatActivityViewModel by viewModels()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
Content(viewModel)
|
val windowSizeClass = calculateWindowSizeClass(this)
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val chatState = rememberChatState(
|
||||||
|
requestInProgress = uiState.requestInProgress,
|
||||||
|
onSend = { message ->
|
||||||
|
viewModel.sendMessage(message)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val threadViewLazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
ChatScreen(
|
||||||
|
windowSizeClass = windowSizeClass,
|
||||||
|
uiState = uiState,
|
||||||
|
chatState = chatState,
|
||||||
|
threadViewLazyListState = threadViewLazyListState,
|
||||||
|
onRequestFocus = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (threadViewLazyListState.layoutInfo.visibleItemsInfo.find { it.key == "composer" } == null) {
|
||||||
|
if (uiState.messages.isNotEmpty()) {
|
||||||
|
threadViewLazyListState.animateScrollToItem(uiState.messages.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chatState.requestFocus()
|
||||||
|
softwareKeyboardController?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.uiEvents.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is ChatActivityUiEvent.ProcessingRequest -> {
|
||||||
|
threadViewLazyListState.animateScrollToItem(uiState.messages.size)
|
||||||
|
}
|
||||||
|
is ChatActivityUiEvent.SuccessfulResponse -> {
|
||||||
|
chatState.composerValue = ""
|
||||||
|
|
||||||
|
if (uiState.messages.isNotEmpty()) {
|
||||||
|
threadViewLazyListState.animateScrollToItem(uiState.messages.size - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
threadViewLazyListState.layoutInfo.visibleItemsInfo.firstOrNull {
|
||||||
|
it.key == "composer"
|
||||||
|
}?.let {
|
||||||
|
chatState.requestFocus()
|
||||||
|
softwareKeyboardController?.show() // TODO perhaps it's pointless to focus since we can click on the toolbar?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ChatActivityUiEvent.Error -> {
|
||||||
|
Toast.makeText(context, event.error, Toast.LENGTH_SHORT)
|
||||||
|
.show() // TODO better way of showing this. snackbar?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Content(
|
fun ChatScreen(
|
||||||
viewModel: ChatActivityViewModel // = viewModel() doesn't work
|
windowSizeClass: WindowSizeClass,
|
||||||
|
uiState: ChatActivityUiState,
|
||||||
|
chatState: ChatState,
|
||||||
|
threadViewLazyListState: LazyListState,
|
||||||
|
onRequestFocus: () -> Unit
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val isTablet = windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact
|
||||||
val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var composerValue by remember { mutableStateOf("") }
|
|
||||||
val composerFocusRequester = remember { FocusRequester() }
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
LaunchedEffect(uiState.requestInProgress) { // TODO probably not the best way
|
|
||||||
if (!uiState.requestInProgress) {
|
|
||||||
if (uiState.requestLastError == null) {
|
|
||||||
composerValue = ""
|
|
||||||
composerFocusRequester.requestFocus() // TODO maybe make toggleable? or smart?
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, uiState.requestLastError, Toast.LENGTH_SHORT).show() // TODO better way of showing this
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!uiState.messageHistory.isEmpty()) {
|
|
||||||
lazyListState.animateScrollToItem(uiState.messageHistory.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatAppTheme {
|
ChatAppTheme {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize().imePadding(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
ChatTopAppBar(uiState.chatTitle ?: "Start a new conversation")
|
ChatTopAppBar(uiState.chatTitle ?: "Start a new conversation")
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
ChatScreenContent(
|
||||||
modifier = Modifier.fillMaxSize().padding(innerPadding),
|
modifier = Modifier.fillMaxSize().padding(innerPadding),
|
||||||
|
isTablet = isTablet,
|
||||||
|
messages = uiState.messages,
|
||||||
|
liveResponse = uiState.liveResponse,
|
||||||
|
chatState = chatState,
|
||||||
|
threadViewLazyListState = threadViewLazyListState,
|
||||||
|
onRequestFocus = onRequestFocus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatScreenContent(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isTablet: Boolean,
|
||||||
|
messages: List<ChatMessage>,
|
||||||
|
liveResponse: String,
|
||||||
|
chatState: ChatState,
|
||||||
|
threadViewLazyListState: LazyListState,
|
||||||
|
onRequestFocus: () -> Unit
|
||||||
|
) {
|
||||||
|
val layout: @Composable (@Composable () -> Unit, @Composable () -> Unit) -> Unit =
|
||||||
|
if (isTablet) {
|
||||||
|
{ thread, composer ->
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) { thread() }
|
||||||
|
Box(modifier = Modifier.width(500.dp)) { composer() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{ thread, composer ->
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) { thread() }
|
||||||
|
composer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(
|
||||||
|
{
|
||||||
|
ThreadView(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp),
|
||||||
|
lazyListState = threadViewLazyListState,
|
||||||
|
messages = messages,
|
||||||
|
liveResponse = liveResponse,
|
||||||
|
chatState = chatState
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
|
.padding(horizontal = 10.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
ChatToolBar(
|
||||||
modifier = Modifier.fillMaxSize().weight(1f),
|
chatState = chatState,
|
||||||
state = lazyListState
|
onEmptySpaceClick = onRequestFocus
|
||||||
) {
|
)
|
||||||
items(uiState.messageHistory) { message ->
|
|
||||||
MessageExchange(
|
|
||||||
modifier = Modifier.padding(5.dp),
|
|
||||||
isComposing = false,
|
|
||||||
composerValue = message.prompt,
|
|
||||||
responseValue = message.response
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
LanguageModelMistakeWarning(
|
||||||
MessageExchange(
|
modifier = Modifier
|
||||||
modifier = Modifier.padding(5.dp).fillParentMaxHeight(),
|
.padding(vertical = 10.dp) // TODO this is troublesome if there's navigation bar below or any kind of padding
|
||||||
isComposing = !uiState.requestInProgress,
|
)
|
||||||
composerValue = composerValue,
|
}
|
||||||
onComposerValueChange = { composerValue = it },
|
}
|
||||||
composerFocusRequester = composerFocusRequester,
|
)
|
||||||
responseValue = uiState.currentMessageResponse // TODO animate this
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
@Composable
|
||||||
modifier = Modifier.fillMaxWidth(),
|
fun ThreadView(
|
||||||
contentAlignment = Alignment.CenterEnd
|
lazyListState: LazyListState,
|
||||||
) {
|
messages: List<ChatMessage>,
|
||||||
Column(
|
liveResponse: String,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
chatState: ChatState,
|
||||||
) {
|
modifier: Modifier = Modifier
|
||||||
ComposerToolBar(
|
) {
|
||||||
modifier = Modifier.width(500.dp).padding(horizontal = 10.dp),
|
val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||||
canSend = composerValue.isNotBlank() && !uiState.requestInProgress,
|
|
||||||
onSend = {
|
|
||||||
viewModel.sendMessage(composerValue)
|
|
||||||
},
|
|
||||||
onEmptySpaceClick = {
|
|
||||||
composerFocusRequester.requestFocus()
|
|
||||||
localSoftwareKeyboardController?.show()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LanguageModelMistakeWarning(
|
val composerValue = if (chatState.requestInProgress) {
|
||||||
modifier = Modifier.padding(vertical = 10.dp)
|
chatState.lastPrompt
|
||||||
)
|
} else {
|
||||||
}
|
chatState.composerValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier
|
||||||
|
.nestedScroll( // Hides the keyboard when scrolling
|
||||||
|
NestedScrollKeyboardHider(localSoftwareKeyboardController)
|
||||||
|
),
|
||||||
|
state = lazyListState
|
||||||
|
) {
|
||||||
|
items(messages) { message ->
|
||||||
|
if (message.role == ChatMessage.Role.User) {
|
||||||
|
ChatMessagePrompt(
|
||||||
|
content = message.content,
|
||||||
|
modifier = Modifier.padding(vertical = 10.dp)
|
||||||
|
)
|
||||||
|
} else if (message.role == ChatMessage.Role.Assistant) {
|
||||||
|
ChatMessageResponse(
|
||||||
|
content = message.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "composer") {
|
||||||
|
ChatMessageComposer(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillParentMaxHeight() // so that you can click anywhere on the screen to focus the text field
|
||||||
|
.disableBringIntoViewOnFocus()
|
||||||
|
.focusRequester(chatState.focusRequester),
|
||||||
|
value = composerValue,
|
||||||
|
onValueChange = {
|
||||||
|
chatState.composerValue = it
|
||||||
|
},
|
||||||
|
submitted = chatState.requestInProgress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatState.requestInProgress) {
|
||||||
|
item {
|
||||||
|
ChatMessageResponse(
|
||||||
|
content = liveResponse
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatMessagePrompt(
|
||||||
|
content: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = content,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatMessageResponse(
|
||||||
|
content: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = content,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatMessageComposer(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
submitted: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val textPadding by animateDpAsState(
|
||||||
|
targetValue = if (submitted) 16.dp else 0.dp,
|
||||||
|
label = "composerTextPaddingAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
|
val textOpacity by animateFloatAsState(
|
||||||
|
targetValue = if (submitted) 0.7f else 1.0f,
|
||||||
|
label = "composerTextOpacityAnimation"
|
||||||
|
)
|
||||||
|
|
||||||
|
SimpleTextFieldWithPadding(
|
||||||
|
modifier = modifier,
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
enabled = !submitted,
|
||||||
|
placeholder = {
|
||||||
|
Text("Type your message...") // TODO hide when just browsing history?
|
||||||
|
},
|
||||||
|
padding = PaddingValues(vertical = 10.dp, horizontal = textPadding),
|
||||||
|
textStyle = LocalTextStyle.current.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = textOpacity)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatToolBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
chatState: ChatState,
|
||||||
|
onEmptySpaceClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val sendButtonColor by animateColorAsState(
|
||||||
|
targetValue = if (chatState.canSend) {
|
||||||
|
IconButtonDefaults.iconButtonColors().contentColor
|
||||||
|
} else {
|
||||||
|
IconButtonDefaults.iconButtonColors().disabledContentColor
|
||||||
|
}, label = "sendButtonColor"
|
||||||
|
)
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onEmptySpaceClick),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = chatState::performSend,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
enabled = chatState.canSend,
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
contentColor = sendButtonColor,
|
||||||
|
disabledContentColor = sendButtonColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||||
|
contentDescription = "Send"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -175,136 +408,8 @@ fun ChatTopAppBar(
|
||||||
) {
|
) {
|
||||||
AnimatedChangingText(
|
AnimatedChangingText(
|
||||||
text = title,
|
text = title,
|
||||||
) // TODO fade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageExchange(
|
|
||||||
isComposing: Boolean,
|
|
||||||
composerValue: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onComposerValueChange: (String) -> Unit = {},
|
|
||||||
composerFocusRequester: FocusRequester = FocusRequester(),
|
|
||||||
responseValue: String = "",
|
|
||||||
) {
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier.padding(horizontal = 10.dp)
|
|
||||||
) {
|
|
||||||
MessageComposer(
|
|
||||||
value = composerValue,
|
|
||||||
onValueChange = onComposerValueChange,
|
|
||||||
enabled = isComposing,
|
|
||||||
focusRequester = composerFocusRequester
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isComposing) {
|
|
||||||
Text(
|
|
||||||
text = responseValue,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageComposer(
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
enabled: Boolean,
|
|
||||||
focusRequester: FocusRequester,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val textFieldPadding by animateDpAsState(
|
|
||||||
targetValue = if (enabled) {
|
|
||||||
0.dp
|
|
||||||
} else {
|
|
||||||
16.dp
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val textFieldTextColor by animateColorAsState(
|
|
||||||
targetValue = if (enabled) {
|
|
||||||
TextFieldDefaults.colors().focusedTextColor
|
|
||||||
} else {
|
|
||||||
TextFieldDefaults.colors().disabledTextColor
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = textFieldPadding)
|
|
||||||
.focusRequester(focusRequester),
|
|
||||||
// .animateContentSize()
|
|
||||||
placeholder = {
|
|
||||||
Text("Type your message...")
|
|
||||||
},
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
unfocusedContainerColor = Color.Transparent,
|
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
disabledContainerColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
disabledIndicatorColor = Color.Transparent,
|
|
||||||
disabledTextColor = textFieldTextColor
|
|
||||||
),
|
|
||||||
enabled = enabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ComposerToolBar(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
canSend: Boolean,
|
|
||||||
onSend: () -> Unit,
|
|
||||||
onEmptySpaceClick: () -> Unit
|
|
||||||
) {
|
|
||||||
val sendButtonColor by animateColorAsState(
|
|
||||||
targetValue = if (canSend) {
|
|
||||||
IconButtonDefaults.iconButtonColors().contentColor
|
|
||||||
} else {
|
|
||||||
IconButtonDefaults.iconButtonColors().disabledContentColor
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ElevatedCard(
|
|
||||||
modifier = modifier.clickable(
|
|
||||||
onClick = onEmptySpaceClick
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(100)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
onSend()
|
|
||||||
},
|
|
||||||
modifier = Modifier.height(48.dp).padding(horizontal = 8.dp),
|
|
||||||
enabled = canSend,
|
|
||||||
colors = IconButtonDefaults.iconButtonColors(
|
|
||||||
contentColor = sendButtonColor,
|
|
||||||
disabledContentColor = sendButtonColor
|
|
||||||
)
|
)
|
||||||
) {
|
|
||||||
Column (
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.Send,
|
|
||||||
contentDescription = "Send"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package eu.m724.chatapp.activity.chat
|
||||||
|
|
||||||
|
sealed interface ChatActivityUiEvent {
|
||||||
|
data object ProcessingRequest : ChatActivityUiEvent
|
||||||
|
data class SuccessfulResponse(val message: String): ChatActivityUiEvent
|
||||||
|
data class Error(val error: String): ChatActivityUiEvent
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package eu.m724.chatapp.activity.chat
|
package eu.m724.chatapp.activity.chat
|
||||||
|
|
||||||
|
import eu.m724.chatapp.api.data.ChatMessage
|
||||||
|
|
||||||
data class ChatActivityUiState(
|
data class ChatActivityUiState(
|
||||||
/**
|
/**
|
||||||
* The title of the current chat
|
* The title of the current chat
|
||||||
|
|
@ -14,20 +16,10 @@ data class ChatActivityUiState(
|
||||||
/**
|
/**
|
||||||
* The response right now, updates when streaming
|
* The response right now, updates when streaming
|
||||||
*/
|
*/
|
||||||
val currentMessageResponse: String = "",
|
val liveResponse: String = "",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All the messages of this chat
|
* All the messages of this chat
|
||||||
*/
|
*/
|
||||||
val messageHistory: List<ChatMessageExchange> = listOf(),
|
val messages: List<ChatMessage> = listOf()
|
||||||
|
|
||||||
/**
|
|
||||||
* Error, if any, of the last request
|
|
||||||
*/
|
|
||||||
val requestLastError: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ChatMessageExchange(
|
|
||||||
val prompt: String,
|
|
||||||
var response: String
|
|
||||||
)
|
)
|
||||||
|
|
@ -6,9 +6,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import eu.m724.chatapp.api.AiApiService
|
import eu.m724.chatapp.api.AiApiService
|
||||||
import eu.m724.chatapp.api.data.ChatMessage
|
import eu.m724.chatapp.api.data.ChatMessage
|
||||||
import eu.m724.chatapp.api.data.request.ChatCompletionRequest
|
import eu.m724.chatapp.api.data.request.ChatCompletionRequest
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -17,32 +19,36 @@ import javax.inject.Inject
|
||||||
class ChatActivityViewModel @Inject constructor(
|
class ChatActivityViewModel @Inject constructor(
|
||||||
private val aiApiService: AiApiService
|
private val aiApiService: AiApiService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private val messages = mutableListOf<ChatMessage>()
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ChatActivityUiState())
|
private val _uiState = MutableStateFlow(ChatActivityUiState())
|
||||||
val uiState: StateFlow<ChatActivityUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ChatActivityUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val messages = mutableListOf<ChatMessage>()
|
private val _uiEvents = Channel<ChatActivityUiEvent>()
|
||||||
|
val uiEvents = _uiEvents.receiveAsFlow()
|
||||||
|
|
||||||
fun sendMessage(message: String) {
|
fun sendMessage(message: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
var uiState = it.copy(
|
||||||
requestInProgress = true,
|
requestInProgress = true,
|
||||||
currentMessageResponse = ""
|
liveResponse = "",
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
if (_uiState.value.chatTitle == null) {
|
if (it.chatTitle == null) {
|
||||||
_uiState.update {
|
uiState = uiState.copy(chatTitle = message)
|
||||||
it.copy(chatTitle = "User says \"$message\"") // TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uiState
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.add(ChatMessage(
|
messages.add(ChatMessage(
|
||||||
role = ChatMessage.Role.USER,
|
role = ChatMessage.Role.User,
|
||||||
content = message
|
content = message
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
_uiEvents.send(ChatActivityUiEvent.ProcessingRequest)
|
||||||
|
|
||||||
val response = aiApiService.chatComplete(ChatCompletionRequest(
|
val response = aiApiService.chatComplete(ChatCompletionRequest(
|
||||||
model = "free-model",
|
model = "free-model",
|
||||||
messages = messages,
|
messages = messages,
|
||||||
|
|
@ -53,13 +59,16 @@ class ChatActivityViewModel @Inject constructor(
|
||||||
))
|
))
|
||||||
|
|
||||||
if (!response.isSuccessful || response.body() == null) {
|
if (!response.isSuccessful || response.body() == null) {
|
||||||
|
messages.removeLast()
|
||||||
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestInProgress = false,
|
requestInProgress = false
|
||||||
requestLastError = response.code().toString()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_uiEvents.send(ChatActivityUiEvent.Error(response.code().toString()))
|
||||||
|
|
||||||
// TODO launch toast or something
|
// TODO launch toast or something
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
@ -72,11 +81,11 @@ class ChatActivityViewModel @Inject constructor(
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestInProgress = false,
|
requestInProgress = false,
|
||||||
messageHistory = it.messageHistory.plus(
|
messages = messages.toList()
|
||||||
ChatMessageExchange(message, choice.message.content)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_uiEvents.send(ChatActivityUiEvent.SuccessfulResponse(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
59
app/src/main/java/eu/m724/chatapp/activity/chat/ChatState.kt
Normal file
59
app/src/main/java/eu/m724/chatapp/activity/chat/ChatState.kt
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package eu.m724.chatapp.activity.chat
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
|
||||||
|
class ChatState(
|
||||||
|
val focusRequester: FocusRequester,
|
||||||
|
private val onSend: (String) -> Unit, // Store the lambda
|
||||||
|
initialRequestInProgress: Boolean
|
||||||
|
) {
|
||||||
|
var composerValue by mutableStateOf("")
|
||||||
|
var lastPrompt by mutableStateOf("")
|
||||||
|
var requestInProgress by mutableStateOf(initialRequestInProgress)
|
||||||
|
|
||||||
|
val canSend: Boolean
|
||||||
|
get() = composerValue.isNotBlank() && !requestInProgress
|
||||||
|
|
||||||
|
// This method will be called by the UI (e.g., the send button)
|
||||||
|
fun performSend() {
|
||||||
|
if (canSend) {
|
||||||
|
lastPrompt = composerValue
|
||||||
|
onSend(composerValue)
|
||||||
|
composerValue = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestFocus() {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Composable
|
||||||
|
fun rememberChatState(
|
||||||
|
requestInProgress: Boolean,
|
||||||
|
onSend: (String) -> Unit, // Takes the message string as a parameter
|
||||||
|
focusRequester: FocusRequester = remember { FocusRequester() }
|
||||||
|
): ChatState {
|
||||||
|
val state = remember {
|
||||||
|
ChatState(
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
onSend = onSend, // Pass the lambda directly
|
||||||
|
initialRequestInProgress = requestInProgress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(requestInProgress) {
|
||||||
|
state.requestInProgress = requestInProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapp.activity.chat.compose
|
package eu.m724.chatapp.activity.chat.composable
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package eu.m724.chatapp.activity.chat.composable
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.layout.LayoutCoordinates
|
||||||
|
import androidx.compose.ui.node.ModifierNodeElement
|
||||||
|
import androidx.compose.ui.platform.InspectorInfo
|
||||||
|
import androidx.compose.ui.relocation.BringIntoViewModifierNode
|
||||||
|
|
||||||
|
private class DisableBringIntoViewOnFocusNode : Modifier.Node(), BringIntoViewModifierNode {
|
||||||
|
// When a child of this modifier requests to be brought into view, this method is called.
|
||||||
|
// By providing an empty implementation, we effectively "swallow" the request
|
||||||
|
// and prevent it from propagating up to the LazyColumn, which would otherwise scroll.
|
||||||
|
override suspend fun bringIntoView(
|
||||||
|
childCoordinates: LayoutCoordinates,
|
||||||
|
boundsProvider: () -> Rect?
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DisableBringIntoViewOnFocusElement : ModifierNodeElement<DisableBringIntoViewOnFocusNode>() {
|
||||||
|
override fun create() = DisableBringIntoViewOnFocusNode()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other === this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return "disableBringIntoViewOnFocus".hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(node: DisableBringIntoViewOnFocusNode) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun InspectorInfo.inspectableProperties() {
|
||||||
|
name = "disableBringIntoViewOnFocus"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the public Modifier function you will use in your code.
|
||||||
|
fun Modifier.disableBringIntoViewOnFocus(): Modifier = this.then(DisableBringIntoViewOnFocusElement())
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.m724.chatapp.activity.chat.compose
|
package eu.m724.chatapp.activity.chat.composable
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package eu.m724.chatapp.activity.chat.composable
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||||
|
|
||||||
|
class NestedScrollKeyboardHider(
|
||||||
|
private val softwareKeyboardController: SoftwareKeyboardController?
|
||||||
|
) : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
if (available.y > 0) // if scrolling up (content up, finger down)
|
||||||
|
softwareKeyboardController?.hide()
|
||||||
|
return Offset.Companion.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package eu.m724.chatapp.activity.chat.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SimpleTextFieldWithPadding(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
enabled: Boolean,
|
||||||
|
placeholder: @Composable () -> Unit,
|
||||||
|
padding: PaddingValues,
|
||||||
|
textStyle: TextStyle,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
textStyle = textStyle,
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface)
|
||||||
|
) { innerTextField ->
|
||||||
|
TextFieldDefaults.DecorationBox(
|
||||||
|
value = value,
|
||||||
|
visualTransformation = VisualTransformation.None,
|
||||||
|
innerTextField = innerTextField,
|
||||||
|
singleLine = false,
|
||||||
|
enabled = true, // to make things easier
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
contentPadding = padding,
|
||||||
|
placeholder = placeholder,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
package eu.m724.chatapp.api.data
|
package eu.m724.chatapp.api.data
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class ChatMessage(
|
data class ChatMessage(
|
||||||
val role: Role,
|
val role: Role,
|
||||||
val content: String
|
val content: String
|
||||||
) {
|
) {
|
||||||
enum class Role {
|
enum class Role {
|
||||||
SYSTEM,
|
@SerializedName("system")
|
||||||
USER,
|
System,
|
||||||
ASSISTANT
|
|
||||||
|
@SerializedName("user")
|
||||||
|
User,
|
||||||
|
|
||||||
|
@SerializedName("assistant")
|
||||||
|
Assistant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,17 +49,20 @@ enum class CompletionFinishReason {
|
||||||
/**
|
/**
|
||||||
* The response has stopped, because the model said so
|
* The response has stopped, because the model said so
|
||||||
*/
|
*/
|
||||||
STOP,
|
@SerializedName("stop")
|
||||||
|
Stop,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The response has stopped, because it got too long
|
* The response has stopped, because it got too long
|
||||||
*/
|
*/
|
||||||
LENGTH,
|
@SerializedName("length")
|
||||||
|
Length,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The response has stopped, because the content got flagged
|
* The response has stopped, because the content got flagged
|
||||||
*/
|
*/
|
||||||
CONTENT_FILTER
|
@SerializedName("content_filter")
|
||||||
|
ContentFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CompletionTokenUsage(
|
data class CompletionTokenUsage(
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ ksp = "2.1.21-2.0.2"
|
||||||
retrofit = "3.0.0"
|
retrofit = "3.0.0"
|
||||||
secrets = "2.0.1"
|
secrets = "2.0.1"
|
||||||
loggingInterceptor = "4.12.0"
|
loggingInterceptor = "4.12.0"
|
||||||
|
material3WindowSizeClass = "1.3.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
|
@ -38,6 +39,7 @@ hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.r
|
||||||
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit"}
|
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit"}
|
||||||
logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "loggingInterceptor" }
|
logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "loggingInterceptor" }
|
||||||
|
androidx-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue