From a293920cbf7eaf516d57f5b6b83c3f879a798986 Mon Sep 17 00:00:00 2001 From: Taewan Park Date: Fri, 12 Jul 2024 17:20:41 +0900 Subject: [PATCH 1/6] Change API message order to ascending --- .../chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt index c2fecf4..748fc62 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt @@ -160,7 +160,7 @@ fun ChatScreen( .horizontalScroll(chatBubbleScrollStates[(key - 1) / 2]) ) { Spacer(modifier = Modifier.width(8.dp)) - groupedMessages[key]!!.sortedByDescending { it.platformType }.forEach { m -> + groupedMessages[key]!!.sortedBy { it.platformType }.forEach { m -> m.platformType?.let { apiType -> OpponentChatBubble( modifier = Modifier @@ -200,7 +200,7 @@ fun ChatScreen( .horizontalScroll(chatBubbleScrollStates[(latestMessageIndex + 1) / 2]) ) { Spacer(modifier = Modifier.width(8.dp)) - chatViewModel.enabledPlatformsInChat.sortedDescending().forEach { apiType -> + chatViewModel.enabledPlatformsInChat.sorted().forEach { apiType -> val message = when (apiType) { ApiType.OPENAI -> openAIMessage ApiType.ANTHROPIC -> anthropicMessage From b2cbe32eb1381b74ec0e16a81e4743c6fe47e5da Mon Sep 17 00:00:00 2001 From: Taewan Park Date: Tue, 16 Jul 2024 17:10:14 +0900 Subject: [PATCH 2/6] Support API Url customization Currently Google API is not supported --- .../gptmobile/data/ModelConstants.kt | 7 +- .../data/datastore/SettingDataSource.kt | 2 + .../data/datastore/SettingDataSourceImpl.kt | 15 ++++ .../gptmobile/data/dto/Platform.kt | 1 + .../gptmobile/data/network/AnthropicAPI.kt | 1 + .../data/network/AnthropicAPIImpl.kt | 9 ++- .../data/repository/ChatRepositoryImpl.kt | 8 ++- .../data/repository/SettingRepositoryImpl.kt | 35 ++++------ .../ui/setting/PlatformSettingDialogs.kt | 69 +++++++++++++++++++ .../ui/setting/PlatformSettingScreen.kt | 19 ++++- .../ui/setting/SettingViewModel.kt | 21 ++++++ .../chungjungsoo/gptmobile/util/Strings.kt | 6 ++ app/src/main/res/drawable/ic_link.xml | 5 ++ app/src/main/res/values-ko-rKR/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 15 files changed, 173 insertions(+), 31 deletions(-) create mode 100644 app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/Strings.kt create mode 100644 app/src/main/res/drawable/ic_link.xml diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/ModelConstants.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/ModelConstants.kt index 54192a6..5a259a3 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/ModelConstants.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/ModelConstants.kt @@ -5,6 +5,9 @@ object ModelConstants { val openaiModels = linkedSetOf("gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo") val anthropicModels = linkedSetOf("claude-3-5-sonnet-20240620", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307") val googleModels = linkedSetOf("gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-1.0-pro") + const val OPENAI_API_URL = "https://api.openai.com" + const val ANTHROPIC_API_URL = "https://api.anthropic.com" + const val GOOGLE_API_URL = "https://generativelanguage.googleapis.com" const val ANTHROPIC_MAXIMUM_TOKEN = 4096 @@ -13,7 +16,5 @@ object ModelConstants { "You are familiar with various languages in the world. " + "You are to answer my questions precisely. " - const val ANTHROPIC_PROMPT = "Your task is to answer my questions precisely." - - const val GOOGLE_PROMPT = "Your task is to answer my questions precisely." + const val DEFAULT_PROMPT = "Your task is to answer my questions precisely." } diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/datastore/SettingDataSource.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/datastore/SettingDataSource.kt index 50fd560..8d37c37 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/datastore/SettingDataSource.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/datastore/SettingDataSource.kt @@ -8,6 +8,7 @@ interface SettingDataSource { suspend fun updateDynamicTheme(theme: DynamicTheme) suspend fun updateThemeMode(themeMode: ThemeMode) suspend fun updateStatus(apiType: ApiType, status: Boolean) + suspend fun updateAPIUrl(apiType: ApiType, url: String) suspend fun updateToken(apiType: ApiType, token: String) suspend fun updateModel(apiType: ApiType, model: String) suspend fun updateTemperature(apiType: ApiType, temperature: Float) @@ -16,6 +17,7 @@ interface SettingDataSource { suspend fun getDynamicTheme(): DynamicTheme? suspend fun getThemeMode(): ThemeMode? suspend fun getStatus(apiType: ApiType): Boolean? + suspend fun getAPIUrl(apiType: ApiType): String? suspend fun getToken(apiType: ApiType): String? suspend fun getModel(apiType: ApiType): String? suspend fun getTemperature(apiType: ApiType): Float? diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/datastore/SettingDataSourceImpl.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/datastore/SettingDataSourceImpl.kt index 72f6da4..8aac419 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/datastore/SettingDataSourceImpl.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/datastore/SettingDataSourceImpl.kt @@ -22,6 +22,11 @@ class SettingDataSourceImpl @Inject constructor( ApiType.ANTHROPIC to booleanPreferencesKey("anthropic_status"), ApiType.GOOGLE to booleanPreferencesKey("google_status") ) + private val apiUrlMap = mapOf( + ApiType.OPENAI to stringPreferencesKey("openai_url"), + ApiType.ANTHROPIC to stringPreferencesKey("anthropic_url"), + ApiType.GOOGLE to stringPreferencesKey("google_url") + ) private val apiTokenMap = mapOf( ApiType.OPENAI to stringPreferencesKey("openai_token"), ApiType.ANTHROPIC to stringPreferencesKey("anthropic_token"), @@ -68,6 +73,12 @@ class SettingDataSourceImpl @Inject constructor( } } + override suspend fun updateAPIUrl(apiType: ApiType, url: String) { + dataStore.edit { pref -> + pref[apiUrlMap[apiType]!!] = url + } + } + override suspend fun updateToken(apiType: ApiType, token: String) { dataStore.edit { pref -> pref[apiTokenMap[apiType]!!] = token @@ -118,6 +129,10 @@ class SettingDataSourceImpl @Inject constructor( pref[apiStatusMap[apiType]!!] }.first() + override suspend fun getAPIUrl(apiType: ApiType): String? = dataStore.data.map { pref -> + pref[apiUrlMap[apiType]!!] + }.first() + override suspend fun getToken(apiType: ApiType): String? = dataStore.data.map { pref -> pref[apiTokenMap[apiType]!!] }.first() diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/dto/Platform.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/dto/Platform.kt index 930e716..4ae17ca 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/dto/Platform.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/dto/Platform.kt @@ -6,6 +6,7 @@ data class Platform( val name: ApiType, val selected: Boolean = false, val enabled: Boolean = false, + val apiUrl: String = "", val token: String? = null, val model: String? = null, val temperature: Float? = null, diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPI.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPI.kt index 947e0e5..944d569 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPI.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPI.kt @@ -6,5 +6,6 @@ import kotlinx.coroutines.flow.Flow interface AnthropicAPI { fun setToken(token: String?) + fun setAPIUrl(url: String) fun streamChatMessage(messageRequest: MessageRequest): Flow } diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPIImpl.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPIImpl.kt index efa117f..738500a 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPIImpl.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPIImpl.kt @@ -1,5 +1,6 @@ package dev.chungjungsoo.gptmobile.data.network +import dev.chungjungsoo.gptmobile.data.ModelConstants import dev.chungjungsoo.gptmobile.data.dto.anthropic.request.MessageRequest import dev.chungjungsoo.gptmobile.data.dto.anthropic.response.ErrorDetail import dev.chungjungsoo.gptmobile.data.dto.anthropic.response.ErrorResponseChunk @@ -32,17 +33,22 @@ class AnthropicAPIImpl @Inject constructor( ) : AnthropicAPI { private var token: String? = null + private var apiUrl: String = ModelConstants.ANTHROPIC_API_URL override fun setToken(token: String?) { this.token = token } + override fun setAPIUrl(url: String) { + this.apiUrl = url + } + override fun streamChatMessage(messageRequest: MessageRequest): Flow { val body = Json.encodeToJsonElement(messageRequest) val builder = HttpRequestBuilder().apply { method = HttpMethod.Post - url("${ANTHROPIC_CHAT_API}/v1/messages") + url("$apiUrl/v1/messages") contentType(ContentType.Application.Json) setBody(body) accept(ContentType.Text.EventStream) @@ -81,7 +87,6 @@ class AnthropicAPIImpl @Inject constructor( } companion object { - private const val ANTHROPIC_CHAT_API = "https://api.anthropic.com" private const val STREAM_PREFIX = "data:" private const val STREAM_END_TOKEN = "event: message_stop" private const val API_KEY_HEADER = "x-api-key" diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt index 6994caa..19cc826 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt @@ -6,6 +6,7 @@ import com.aallam.openai.api.chat.ChatMessage import com.aallam.openai.api.chat.ChatRole import com.aallam.openai.api.model.ModelId import com.aallam.openai.client.OpenAI +import com.aallam.openai.client.OpenAIHost import com.google.ai.client.generativeai.GenerativeModel import com.google.ai.client.generativeai.type.BlockThreshold import com.google.ai.client.generativeai.type.Content @@ -48,7 +49,7 @@ class ChatRepositoryImpl @Inject constructor( override suspend fun completeOpenAIChat(question: Message, history: List): Flow { val platform = checkNotNull(settingRepository.fetchPlatforms().firstOrNull { it.name == ApiType.OPENAI }) - openAI = OpenAI(platform.token ?: "") + openAI = OpenAI(platform.token ?: "", host = OpenAIHost(baseUrl = platform.apiUrl)) val generatedMessages = messageToOpenAIMessage(history + listOf(question)) val generatedMessageWithPrompt = listOf( @@ -71,13 +72,14 @@ class ChatRepositoryImpl @Inject constructor( override suspend fun completeAnthropicChat(question: Message, history: List): Flow { val platform = checkNotNull(settingRepository.fetchPlatforms().firstOrNull { it.name == ApiType.ANTHROPIC }) anthropic.setToken(platform.token) + anthropic.setAPIUrl(platform.apiUrl) val generatedMessages = messageToAnthropicMessage(history + listOf(question)) val messageRequest = MessageRequest( model = platform.model ?: "", messages = generatedMessages, maxTokens = ModelConstants.ANTHROPIC_MAXIMUM_TOKEN, - systemPrompt = platform.systemPrompt ?: ModelConstants.ANTHROPIC_PROMPT, + systemPrompt = platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT, stream = true, temperature = platform.temperature, topP = platform.topP @@ -105,7 +107,7 @@ class ChatRepositoryImpl @Inject constructor( google = GenerativeModel( modelName = platform.model ?: "", apiKey = platform.token ?: "", - systemInstruction = content { text(platform.systemPrompt ?: ModelConstants.GOOGLE_PROMPT) }, + systemInstruction = content { text(platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT) }, generationConfig = config, safetySettings = listOf( SafetySetting(HarmCategory.DANGEROUS_CONTENT, BlockThreshold.ONLY_HIGH), diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepositoryImpl.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepositoryImpl.kt index cefa3bd..237292c 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepositoryImpl.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepositoryImpl.kt @@ -15,19 +15,25 @@ class SettingRepositoryImpl @Inject constructor( override suspend fun fetchPlatforms(): List = ApiType.entries.map { apiType -> val status = settingDataSource.getStatus(apiType) + val apiUrl = when (apiType) { + ApiType.OPENAI -> settingDataSource.getAPIUrl(apiType) ?: ModelConstants.OPENAI_API_URL + ApiType.ANTHROPIC -> settingDataSource.getAPIUrl(apiType) ?: ModelConstants.ANTHROPIC_API_URL + ApiType.GOOGLE -> settingDataSource.getAPIUrl(apiType) ?: ModelConstants.GOOGLE_API_URL + } val token = settingDataSource.getToken(apiType) val model = settingDataSource.getModel(apiType) val temperature = settingDataSource.getTemperature(apiType) val topP = settingDataSource.getTopP(apiType) val systemPrompt = when (apiType) { ApiType.OPENAI -> settingDataSource.getSystemPrompt(ApiType.OPENAI) ?: ModelConstants.OPENAI_PROMPT - ApiType.ANTHROPIC -> settingDataSource.getSystemPrompt(ApiType.ANTHROPIC) ?: ModelConstants.ANTHROPIC_PROMPT - ApiType.GOOGLE -> settingDataSource.getSystemPrompt(ApiType.GOOGLE) ?: ModelConstants.GOOGLE_PROMPT + ApiType.ANTHROPIC -> settingDataSource.getSystemPrompt(ApiType.ANTHROPIC) ?: ModelConstants.DEFAULT_PROMPT + ApiType.GOOGLE -> settingDataSource.getSystemPrompt(ApiType.GOOGLE) ?: ModelConstants.DEFAULT_PROMPT } Platform( name = apiType, enabled = status ?: false, + apiUrl = apiUrl, token = token, model = model, temperature = temperature, @@ -44,26 +50,13 @@ class SettingRepositoryImpl @Inject constructor( override suspend fun updatePlatforms(platforms: List) { platforms.forEach { platform -> settingDataSource.updateStatus(platform.name, platform.enabled) + settingDataSource.updateAPIUrl(platform.name, platform.apiUrl) - if (platform.token != null) { - settingDataSource.updateToken(platform.name, platform.token) - } - - if (platform.model != null) { - settingDataSource.updateModel(platform.name, platform.model) - } - - if (platform.temperature != null) { - settingDataSource.updateTemperature(platform.name, platform.temperature) - } - - if (platform.topP != null) { - settingDataSource.updateTopP(platform.name, platform.topP) - } - - if (platform.systemPrompt != null) { - settingDataSource.updateSystemPrompt(platform.name, platform.systemPrompt.trim()) - } + platform.token?.let { settingDataSource.updateToken(platform.name, it) } + platform.model?.let { settingDataSource.updateModel(platform.name, it) } + platform.temperature?.let { settingDataSource.updateTemperature(platform.name, it) } + platform.topP?.let { settingDataSource.updateTopP(platform.name, it) } + platform.systemPrompt?.let { settingDataSource.updateSystemPrompt(platform.name, it.trim()) } } } diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt index 2fcf6cc..1b7137d 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt @@ -37,8 +37,27 @@ import dev.chungjungsoo.gptmobile.util.generateGoogleModelList import dev.chungjungsoo.gptmobile.util.generateOpenAIModelList import dev.chungjungsoo.gptmobile.util.getPlatformAPILabelResources import dev.chungjungsoo.gptmobile.util.getPlatformHelpLinkResources +import dev.chungjungsoo.gptmobile.util.isValidUrl import kotlin.math.roundToInt +@Composable +fun APIUrlDialog( + dialogState: SettingViewModel.DialogState, + apiType: ApiType, + settingViewModel: SettingViewModel +) { + if (dialogState.isApiUrlDialogOpen) { + APIUrlDialog( + apiType = apiType, + onDismissRequest = settingViewModel::closeApiUrlDialog + ) { apiUrl -> + settingViewModel.updateURL(apiType, apiUrl) + settingViewModel.savePlatformSettings() + settingViewModel.closeApiUrlDialog() + } + } +} + @Composable fun APIKeyDialog( dialogState: SettingViewModel.DialogState, @@ -136,6 +155,56 @@ fun SystemPromptDialog( } } +@Composable +private fun APIUrlDialog( + apiType: ApiType, + onDismissRequest: () -> Unit, + onConfirmRequest: (url: String) -> Unit +) { + var apiUrl by remember { mutableStateOf("") } + val configuration = LocalConfiguration.current + + AlertDialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + modifier = Modifier.widthIn(max = configuration.screenWidthDp.dp - 40.dp), + title = { Text(text = stringResource(R.string.api_url)) }, + text = { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + value = apiUrl, + isError = apiUrl.isValidUrl(), + onValueChange = { apiUrl = it }, + label = { + Text(stringResource(R.string.api_url)) + }, + supportingText = { + if (apiUrl.isValidUrl().not()) { + Text(text = stringResource(R.string.invalid_api_url)) + } + } + ) + }, + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + enabled = apiUrl.isNotBlank() && apiUrl.isValidUrl(), + onClick = { onConfirmRequest(apiUrl) } + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton( + onClick = onDismissRequest + ) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + @Composable private fun APIKeyDialog( apiType: ApiType, diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt index 949808b..ad0e862 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt @@ -87,14 +87,29 @@ fun PlatformSettingScreen( val topP = platform?.topP val systemPrompt = platform?.systemPrompt ?: when (apiType) { ApiType.OPENAI -> ModelConstants.OPENAI_PROMPT - ApiType.ANTHROPIC -> ModelConstants.ANTHROPIC_PROMPT - ApiType.GOOGLE -> ModelConstants.GOOGLE_PROMPT + ApiType.ANTHROPIC -> ModelConstants.DEFAULT_PROMPT + ApiType.GOOGLE -> ModelConstants.DEFAULT_PROMPT } PreferenceSwitchWithContainer( title = stringResource(R.string.enable_api), isChecked = enabled ) { settingViewModel.toggleAPI(apiType) } + SettingItem( + modifier = Modifier.height(64.dp), + title = stringResource(R.string.api_url), + description = "", + enabled = enabled, + onItemClick = settingViewModel::openApiUrlDialog, + showTrailingIcon = false, + showLeadingIcon = true, + leadingIcon = { + Icon( + ImageVector.vectorResource(id = R.drawable.ic_link), + contentDescription = stringResource(R.string.url_icon) + ) + } + ) SettingItem( modifier = Modifier.height(64.dp), title = stringResource(R.string.api_key), diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/SettingViewModel.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/SettingViewModel.kt index 3b4d11a..1ed6d1b 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/SettingViewModel.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/SettingViewModel.kt @@ -54,6 +54,22 @@ class SettingViewModel @Inject constructor( } } + fun updateURL(apiType: ApiType, url: String) { + val index = _platformState.value.indexOfFirst { it.name == apiType } + + if (index >= 0) { + _platformState.update { + it.mapIndexed { i, p -> + if (index == i && url.isNotBlank()) { + p.copy(apiUrl = url) + } else { + p + } + } + } + } + } + fun updateToken(apiType: ApiType, token: String) { val index = _platformState.value.indexOfFirst { it.name == apiType } @@ -146,6 +162,8 @@ class SettingViewModel @Inject constructor( fun openThemeDialog() = _dialogState.update { it.copy(isThemeDialogOpen = true) } + fun openApiUrlDialog() = _dialogState.update { it.copy(isApiUrlDialogOpen = true) } + fun openApiTokenDialog() = _dialogState.update { it.copy(isApiTokenDialogOpen = true) } fun openApiModelDialog() = _dialogState.update { it.copy(isApiModelDialogOpen = true) } @@ -158,6 +176,8 @@ class SettingViewModel @Inject constructor( fun closeThemeDialog() = _dialogState.update { it.copy(isThemeDialogOpen = false) } + fun closeApiUrlDialog() = _dialogState.update { it.copy(isApiUrlDialogOpen = false) } + fun closeApiTokenDialog() = _dialogState.update { it.copy(isApiTokenDialogOpen = false) } fun closeApiModelDialog() = _dialogState.update { it.copy(isApiModelDialogOpen = false) } @@ -177,6 +197,7 @@ class SettingViewModel @Inject constructor( data class DialogState( val isThemeDialogOpen: Boolean = false, + val isApiUrlDialogOpen: Boolean = false, val isApiTokenDialogOpen: Boolean = false, val isApiModelDialogOpen: Boolean = false, val isTemperatureDialogOpen: Boolean = false, diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/Strings.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/Strings.kt new file mode 100644 index 0000000..ce92014 --- /dev/null +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/Strings.kt @@ -0,0 +1,6 @@ +package dev.chungjungsoo.gptmobile.util + +import android.util.Patterns +import android.webkit.URLUtil + +fun String.isValidUrl(): Boolean = URLUtil.isValidUrl(this) && Patterns.WEB_URL.matcher(this).matches() diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml new file mode 100644 index 0000000..ea47c63 --- /dev/null +++ b/app/src/main/res/drawable/ic_link.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 4ad5475..971f8f4 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -107,4 +107,7 @@ GitHub 아이콘 F-Droid 아이콘 Play Store 아이콘 + API 주소 + 주소 아이콘 + 유효한 주소를 입력해 주세요. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4542fc2..2d5af01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -133,4 +133,7 @@ https://f-droid.org/packages/dev.chungjungsoo.gptmobile/ https://play.google.com/store/apps/details?id=dev.chungjungsoo.gptmobile https://github.com/Taewan-P/gpt_mobile/issues/new + API URL + URL Icon + Please enter a valid URL. \ No newline at end of file From 2d7ec43727692c7b0ad2103e07c4625980cc2cf7 Mon Sep 17 00:00:00 2001 From: Taewan Park Date: Sat, 10 Aug 2024 17:21:11 +0900 Subject: [PATCH 3/6] Disable custom url setting for Google --- .../gptmobile/presentation/ui/setting/PlatformSettingScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt index ad0e862..1ebc254 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt @@ -99,7 +99,7 @@ fun PlatformSettingScreen( modifier = Modifier.height(64.dp), title = stringResource(R.string.api_url), description = "", - enabled = enabled, + enabled = enabled && platform?.name != ApiType.GOOGLE, onItemClick = settingViewModel::openApiUrlDialog, showTrailingIcon = false, showLeadingIcon = true, From 1d82160171265fb71050ffd9254f83628a27416e Mon Sep 17 00:00:00 2001 From: Taewan Park Date: Fri, 16 Aug 2024 13:55:02 +0900 Subject: [PATCH 4/6] Enable unsecure http connection --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 39e9bb5..1e47f7c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ android:icon="@mipmap/ic_gpt_mobile" android:label="@string/app_name" android:supportsRtl="true" + android:usesCleartextTraffic="true" tools:targetApi="upside_down_cake"> Date: Fri, 16 Aug 2024 13:55:17 +0900 Subject: [PATCH 5/6] Upgrade AGP version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7f35e29..38bf695 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.5.0" +agp = "8.5.2" autoLicense = "11.2.2" kotlin = "1.9.23" coreKtx = "1.13.1" From 0f9bcef896c2c8d5f0e2d0da6c5505aaf1d9b85f Mon Sep 17 00:00:00 2001 From: Taewan Park Date: Fri, 16 Aug 2024 14:44:48 +0900 Subject: [PATCH 6/6] Implement missing dialog --- .../gptmobile/data/ModelConstants.kt | 8 ++++ .../gptmobile/data/dto/Platform.kt | 3 +- .../ui/setting/PlatformSettingDialogs.kt | 42 +++++++++++++------ .../ui/setting/PlatformSettingScreen.kt | 4 +- app/src/main/res/values-ko-rKR/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 45 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/ModelConstants.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/ModelConstants.kt index 5a259a3..0992e8e 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/ModelConstants.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/ModelConstants.kt @@ -1,5 +1,7 @@ package dev.chungjungsoo.gptmobile.data +import dev.chungjungsoo.gptmobile.data.model.ApiType + object ModelConstants { // LinkedHashSet should be used to guarantee item order val openaiModels = linkedSetOf("gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo") @@ -9,6 +11,12 @@ object ModelConstants { const val ANTHROPIC_API_URL = "https://api.anthropic.com" const val GOOGLE_API_URL = "https://generativelanguage.googleapis.com" + fun getDefaultAPIUrl(apiType: ApiType) = when (apiType) { + ApiType.OPENAI -> OPENAI_API_URL + ApiType.ANTHROPIC -> ANTHROPIC_API_URL + ApiType.GOOGLE -> GOOGLE_API_URL + } + const val ANTHROPIC_MAXIMUM_TOKEN = 4096 const val OPENAI_PROMPT = diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/dto/Platform.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/dto/Platform.kt index 4ae17ca..ef3a76f 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/dto/Platform.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/dto/Platform.kt @@ -1,12 +1,13 @@ package dev.chungjungsoo.gptmobile.data.dto +import dev.chungjungsoo.gptmobile.data.ModelConstants.getDefaultAPIUrl import dev.chungjungsoo.gptmobile.data.model.ApiType data class Platform( val name: ApiType, val selected: Boolean = false, val enabled: Boolean = false, - val apiUrl: String = "", + val apiUrl: String = getDefaultAPIUrl(name), val token: String? = null, val model: String? = null, val temperature: Float? = null, diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt index 1b7137d..5f18e04 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt @@ -1,6 +1,7 @@ package dev.chungjungsoo.gptmobile.presentation.ui.setting import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn @@ -27,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import dev.chungjungsoo.gptmobile.R import dev.chungjungsoo.gptmobile.data.ModelConstants.anthropicModels +import dev.chungjungsoo.gptmobile.data.ModelConstants.getDefaultAPIUrl import dev.chungjungsoo.gptmobile.data.ModelConstants.googleModels import dev.chungjungsoo.gptmobile.data.ModelConstants.openaiModels import dev.chungjungsoo.gptmobile.data.model.ApiType @@ -44,17 +46,25 @@ import kotlin.math.roundToInt fun APIUrlDialog( dialogState: SettingViewModel.DialogState, apiType: ApiType, + initialValue: String, settingViewModel: SettingViewModel ) { if (dialogState.isApiUrlDialogOpen) { APIUrlDialog( apiType = apiType, - onDismissRequest = settingViewModel::closeApiUrlDialog - ) { apiUrl -> - settingViewModel.updateURL(apiType, apiUrl) - settingViewModel.savePlatformSettings() - settingViewModel.closeApiUrlDialog() - } + initialValue = initialValue, + onDismissRequest = settingViewModel::closeApiUrlDialog, + onResetRequest = { + settingViewModel.updateURL(apiType, getDefaultAPIUrl(apiType)) + settingViewModel.savePlatformSettings() + settingViewModel.closeApiUrlDialog() + }, + onConfirmRequest = { apiUrl -> + settingViewModel.updateURL(apiType, apiUrl) + settingViewModel.savePlatformSettings() + settingViewModel.closeApiUrlDialog() + } + ) } } @@ -158,10 +168,12 @@ fun SystemPromptDialog( @Composable private fun APIUrlDialog( apiType: ApiType, + initialValue: String, onDismissRequest: () -> Unit, + onResetRequest: () -> Unit, onConfirmRequest: (url: String) -> Unit ) { - var apiUrl by remember { mutableStateOf("") } + var apiUrl by remember { mutableStateOf(initialValue) } val configuration = LocalConfiguration.current AlertDialog( @@ -174,7 +186,7 @@ private fun APIUrlDialog( .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 16.dp), value = apiUrl, - isError = apiUrl.isValidUrl(), + isError = apiUrl.isValidUrl().not(), onValueChange = { apiUrl = it }, label = { Text(stringResource(R.string.api_url)) @@ -196,10 +208,16 @@ private fun APIUrlDialog( } }, dismissButton = { - TextButton( - onClick = onDismissRequest - ) { - Text(stringResource(R.string.cancel)) + Row { + TextButton( + modifier = Modifier.padding(end = 8.dp), + onClick = onResetRequest + ) { + Text(stringResource(R.string.reset)) + } + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } } } ) diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt index 1ebc254..2b59174 100644 --- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt +++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt @@ -80,6 +80,7 @@ fun PlatformSettingScreen( .verticalScroll(scrollState) ) { val platform = platformState.firstOrNull { it.name == apiType } + val url = platform?.apiUrl ?: ModelConstants.getDefaultAPIUrl(apiType) val enabled = platform?.enabled ?: false val model = platform?.model val token = platform?.token @@ -98,7 +99,7 @@ fun PlatformSettingScreen( SettingItem( modifier = Modifier.height(64.dp), title = stringResource(R.string.api_url), - description = "", + description = url, enabled = enabled && platform?.name != ApiType.GOOGLE, onItemClick = settingViewModel::openApiUrlDialog, showTrailingIcon = false, @@ -186,6 +187,7 @@ fun PlatformSettingScreen( } ) + APIUrlDialog(dialogState, apiType, url, settingViewModel) APIKeyDialog(dialogState, apiType, settingViewModel) ModelDialog(dialogState, apiType, model, settingViewModel) TemperatureDialog(dialogState, apiType, temperature, settingViewModel) diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 971f8f4..18aef3c 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -110,4 +110,5 @@ API 주소 주소 아이콘 유효한 주소를 입력해 주세요. + 초기화 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d5af01..e10529c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -136,4 +136,5 @@ API URL URL Icon Please enter a valid URL. + Reset \ No newline at end of file