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
2 changes: 1 addition & 1 deletion .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package dev.chungjungsoo.gptmobile.presentation.ui.chat

import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand Down Expand Up @@ -70,13 +73,15 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider.getUriForFile
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chungjungsoo.gptmobile.R
import dev.chungjungsoo.gptmobile.data.database.entity.Message
import dev.chungjungsoo.gptmobile.data.model.ApiType
import dev.chungjungsoo.gptmobile.util.DefaultHashMap
import dev.chungjungsoo.gptmobile.util.multiScrollStateSaver
import java.io.File
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -115,29 +120,25 @@ fun ChatScreen(
val question by chatViewModel.question.collectAsStateWithLifecycle()
val appEnabledPlatforms by chatViewModel.enabledPlatformsInApp.collectAsStateWithLifecycle()
val editedQuestion by chatViewModel.editedQuestion.collectAsStateWithLifecycle()

val openaiLoadingState by chatViewModel.openaiLoadingState.collectAsStateWithLifecycle()
val anthropicLoadingState by chatViewModel.anthropicLoadingState.collectAsStateWithLifecycle()
val googleLoadingState by chatViewModel.googleLoadingState.collectAsStateWithLifecycle()
val groqLoadingState by chatViewModel.groqLoadingState.collectAsStateWithLifecycle()
val ollamaLoadingState by chatViewModel.ollamaLoadingState.collectAsStateWithLifecycle()
val geminiNanoLoadingState by chatViewModel.geminiNanoLoadingState.collectAsStateWithLifecycle()

val userMessage by chatViewModel.userMessage.collectAsStateWithLifecycle()

val openAIMessage by chatViewModel.openAIMessage.collectAsStateWithLifecycle()
val anthropicMessage by chatViewModel.anthropicMessage.collectAsStateWithLifecycle()
val googleMessage by chatViewModel.googleMessage.collectAsStateWithLifecycle()
val groqMessage by chatViewModel.groqMessage.collectAsStateWithLifecycle()
val ollamaMessage by chatViewModel.ollamaMessage.collectAsStateWithLifecycle()

val geminiNano by chatViewModel.geminiNanoMessage.collectAsStateWithLifecycle()

val canUseChat = (chatViewModel.enabledPlatformsInChat.toSet() - appEnabledPlatforms.toSet()).isEmpty()
val groupedMessages = remember(messages) { groupMessages(messages) }
val latestMessageIndex = groupedMessages.keys.maxOrNull() ?: 0
val chatBubbleScrollStates = rememberSaveable(saver = multiScrollStateSaver) { DefaultHashMap<Int, ScrollState> { ScrollState(0) } }
val canEnableAICoreMode = rememberSaveable { checkAICoreAvailability(aiCorePackageInfo, privateComputePackageInfo) }
val context = LocalContext.current

val scope = rememberCoroutineScope()

Expand All @@ -160,7 +161,16 @@ fun ChatScreen(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { focusManager.clearFocus() },
topBar = { ChatTopBar(chatRoom.title, chatRoom.id > 0, onBackAction, scrollBehavior, chatViewModel::openChatTitleDialog) },
topBar = {
ChatTopBar(
chatRoom.title,
chatRoom.id > 0,
onBackAction,
scrollBehavior,
chatViewModel::openChatTitleDialog,
onExportChatItemClick = { exportChat(context, chatViewModel) }
)
},
bottomBar = {
ChatInputBox(
value = question,
Expand Down Expand Up @@ -366,10 +376,11 @@ private fun groupMessages(messages: List<Message>): HashMap<Int, MutableList<Mes
@OptIn(ExperimentalMaterial3Api::class)
private fun ChatTopBar(
title: String,
isChatTitleUpdateEnabled: Boolean,
isMenuItemEnabled: Boolean,
onBackAction: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
onChatTitleItemClick: () -> Unit
onChatTitleItemClick: () -> Unit,
onExportChatItemClick: () -> Unit
) {
var isDropDownMenuExpanded by remember { mutableStateOf(false) }

Expand All @@ -391,12 +402,13 @@ private fun ChatTopBar(

ChatDropdownMenu(
isDropDownMenuExpanded = isDropDownMenuExpanded,
isChatTitleUpdateEnabled = isChatTitleUpdateEnabled,
isMenuItemEnabled = isMenuItemEnabled,
onDismissRequest = { isDropDownMenuExpanded = false },
onChatTitleItemClick = {
onChatTitleItemClick.invoke()
isDropDownMenuExpanded = false
}
},
onExportChatItemClick = onExportChatItemClick
)
},
scrollBehavior = scrollBehavior
Expand All @@ -406,20 +418,52 @@ private fun ChatTopBar(
@Composable
fun ChatDropdownMenu(
isDropDownMenuExpanded: Boolean,
isChatTitleUpdateEnabled: Boolean,
isMenuItemEnabled: Boolean,
onDismissRequest: () -> Unit,
onChatTitleItemClick: () -> Unit
onChatTitleItemClick: () -> Unit,
onExportChatItemClick: () -> Unit
) {
DropdownMenu(
modifier = Modifier.wrapContentSize(),
expanded = isDropDownMenuExpanded,
onDismissRequest = onDismissRequest
) {
DropdownMenuItem(
enabled = isChatTitleUpdateEnabled,
enabled = isMenuItemEnabled,
text = { Text(text = stringResource(R.string.update_chat_title)) },
onClick = onChatTitleItemClick
)
/* Export Chat */
DropdownMenuItem(
enabled = isMenuItemEnabled,
text = { Text(text = stringResource(R.string.export_chat)) },
onClick = {
onExportChatItemClick()
onDismissRequest()
}
)
}
}

private fun exportChat(context: Context, chatViewModel: ChatViewModel) {
try {
val (fileName, fileContent) = chatViewModel.exportChat()
val file = File(context.getExternalFilesDir(null), fileName)
file.writeText(fileContent)
val uri = getUriForFile(context, "${context.packageName}.fileprovider", file)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/markdown"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(shareIntent, "Share Chat Export").apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
} catch (e: Exception) {
Log.e("ChatExport", "Failed to export chat", e)
Toast.makeText(context, "Failed to export chat", Toast.LENGTH_SHORT).show()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class ChatViewModel @Inject constructor(
private val chatRepository: ChatRepository,
private val settingRepository: SettingRepository
) : ViewModel() {

sealed class LoadingState {
data object Idle : LoadingState()
data object Loading : LoadingState()
Expand Down Expand Up @@ -230,6 +229,36 @@ class ChatViewModel @Inject constructor(
}
}

fun exportChat(): Pair<String, String> {
// Build the chat history in Markdown format
val chatHistoryMarkdown = buildString {
appendLine("# Chat Export: \"${chatRoom.value.title}\"")
appendLine()
appendLine("**Exported on:** ${formatCurrentDateTime()}")
appendLine()
appendLine("---")
appendLine()
appendLine("## Chat History")
appendLine()
messages.value.forEach { message ->
val sender = if (message.platformType == null) "User" else "Assistant"
appendLine("**$sender:**")
appendLine(message.content)
appendLine()
}
}

// Save the Markdown file
val fileName = "export_${chatRoom.value.title}_${System.currentTimeMillis()}.md"
return Pair(fileName, chatHistoryMarkdown)
}

private fun formatCurrentDateTime(): String {
val currentDate = java.util.Date()
val format = java.text.SimpleDateFormat("yyyy-MM-dd hh:mm a", java.util.Locale.getDefault())
return format.format(currentDate)
}

fun updateQuestion(q: String) = _question.update { q }

private fun addMessage(message: Message) = _messages.update { it + listOf(message) }
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,5 @@
<string name="edit">Edit</string>
<string name="edit_question">Edit User Message</string>
<string name="user_message">User Message</string>
<string name="export_chat">Export Chat</string>
</resources>
6 changes: 6 additions & 0 deletions app/src/main/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="external_files"
path="." />
</paths>
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
agp = "8.7.2"
agp = "8.7.3"
autoLicense = "11.2.2"
kotlin = "2.0.20"
coreKtx = "1.15.0"
Expand Down
Loading