├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── playaroundwithgpt │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── playaroundwithgpt │ │ │ ├── data │ │ │ ├── BaseApp.kt │ │ │ ├── Resource.kt │ │ │ ├── Status.kt │ │ │ ├── model │ │ │ │ ├── Content.kt │ │ │ │ ├── DataResponse.kt │ │ │ │ ├── GPTChatRequest.kt │ │ │ │ ├── GPTContent.kt │ │ │ │ ├── GPTMessage.kt │ │ │ │ ├── Message.kt │ │ │ │ └── Metadata.kt │ │ │ ├── remote │ │ │ │ ├── retrofit │ │ │ │ │ ├── HeaderInterceptor.kt │ │ │ │ │ └── RetrofitBuilder.kt │ │ │ │ └── service │ │ │ │ │ └── GptService.kt │ │ │ └── repository │ │ │ │ └── GptRepository.kt │ │ │ ├── di │ │ │ ├── NetworkingModule.kt │ │ │ └── RepositoryModule.kt │ │ │ ├── ui │ │ │ ├── GptViewModel.kt │ │ │ ├── ListAdapter.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MessageItem.kt │ │ │ ├── Typewriter.kt │ │ │ └── base │ │ │ │ └── Adapter.kt │ │ │ └── utils │ │ │ ├── Event.kt │ │ │ └── Extentions.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── simple_list_item_1.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── example │ └── playaroundwithgpt │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | namespace 'com.example.playaroundwithgpt' 10 | compileSdk 33 11 | 12 | defaultConfig { 13 | applicationId "com.example.playaroundwithgpt" 14 | minSdk 24 15 | targetSdk 33 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary true 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | buildFeatures { 39 | dataBinding true 40 | viewBinding true 41 | compose true 42 | } 43 | composeOptions { 44 | kotlinCompilerExtensionVersion '1.3.2' 45 | } 46 | packagingOptions { 47 | resources { 48 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 49 | } 50 | } 51 | } 52 | 53 | dependencies { 54 | 55 | implementation 'androidx.core:core-ktx:1.8.0' 56 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 57 | implementation 'androidx.activity:activity-compose:1.5.1' 58 | implementation platform('androidx.compose:compose-bom:2022.10.00') 59 | implementation 'androidx.compose.ui:ui' 60 | implementation 'androidx.compose.ui:ui-graphics' 61 | implementation 'androidx.compose.ui:ui-tooling-preview' 62 | implementation 'androidx.compose.material3:material3' 63 | implementation 'androidx.appcompat:appcompat:1.5.1' 64 | implementation 'com.google.android.material:material:1.4.0' 65 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 66 | testImplementation 'junit:junit:4.13.2' 67 | implementation "androidx.navigation:navigation-fragment-ktx:$navVersion" 68 | androidTestImplementation 'androidx.test.ext:junit:1.1.4' 69 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' 70 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') 71 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4' 72 | debugImplementation 'androidx.compose.ui:ui-tooling' 73 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 74 | implementation 'androidx.fragment:fragment-ktx:1.5.5' 75 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 76 | implementation 'androidx.activity:activity-ktx:1.6.1' 77 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' 78 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' 79 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' 80 | // hilt 81 | implementation "com.google.dagger:hilt-android:$hiltVersion" 82 | implementation "androidx.hilt:hilt-navigation-compose:$hiltAndroidXVersion" 83 | kapt "com.google.dagger:hilt-compiler:$hiltVersion" 84 | 85 | // Networking 86 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' 87 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 88 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 89 | implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0' 90 | 91 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/playaroundwithgpt/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.playaroundwithgpt", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/BaseApp.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class BaseApp : Application() { 8 | override fun onCreate() { 9 | super.onCreate() 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data 2 | 3 | 4 | data class Resource( 5 | val status: Status, 6 | val data: T?, 7 | val message: String?, 8 | 9 | ) { 10 | companion object { 11 | fun success(data: T?, message: String? = ""): Resource = 12 | Resource(status = Status.SUCCESS, data = data, message = message) 13 | 14 | fun error( 15 | data: T?, 16 | message: String?, 17 | ): Resource = 18 | Resource(status = Status.ERROR, data = data, message = message ?: "") 19 | 20 | fun loading(data: T?): Resource = 21 | Resource(status = Status.LOADING, data = data, message = null) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/Status.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data 2 | 3 | enum class Status { 4 | SUCCESS, 5 | ERROR, 6 | LOADING 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/model/Content.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.model 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Content( 7 | @SerializedName("content_type") 8 | val contentType: String? = "", 9 | @SerializedName("parts") 10 | val parts: List? = listOf() 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/model/DataResponse.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.model 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class DataResponse( 7 | @SerializedName("conversation_id") 8 | val conversationId: String? = "", 9 | @SerializedName("error") 10 | val error: Any? = Any(), 11 | @SerializedName("message") 12 | val message: Message? = Message() 13 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/model/GPTChatRequest.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.model 2 | 3 | import com.example.playaroundwithgpt.data.model.GPTMessage 4 | 5 | 6 | data class GPTChatRequest( 7 | val action: String = "next", 8 | val messages: List, 9 | val conversation_id: String, 10 | val parent_message_id: String, 11 | val model: String = "text-davinci-002-render" 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/model/GPTContent.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.model 2 | 3 | data class GPTContent( 4 | val content_type: String = "text", 5 | val parts: List = listOf() 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/model/GPTMessage.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.model 2 | 3 | data class GPTMessage( 4 | val id: String, 5 | val role: String = "user", 6 | val content: GPTContent = GPTContent() 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/model/Message.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.model 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Message( 7 | @SerializedName("content") 8 | val content: Content? = Content(), 9 | @SerializedName("create_time") 10 | val createTime: Any? = Any(), 11 | @SerializedName("end_turn") 12 | val endTurn: Any? = Any(), 13 | @SerializedName("id") 14 | val id: String? = "", 15 | @SerializedName("metadata") 16 | val metadata: Metadata? = Metadata(), 17 | @SerializedName("recipient") 18 | val recipient: String? = "", 19 | @SerializedName("role") 20 | val role: String? = "", 21 | @SerializedName("update_time") 22 | val updateTime: Any? = Any(), 23 | @SerializedName("user") 24 | val user: Any? = Any(), 25 | @SerializedName("weight") 26 | val weight: Double? = 0.0 27 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/model/Metadata.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.model 2 | 3 | 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class Metadata -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/remote/retrofit/HeaderInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.remote.retrofit 2 | 3 | import android.content.Context 4 | import dagger.hilt.android.qualifiers.ApplicationContext 5 | import okhttp3.Interceptor 6 | import okhttp3.Response 7 | import javax.inject.Inject 8 | 9 | class HeaderInterceptor @Inject constructor( 10 | @ApplicationContext private val context: Context 11 | ) : Interceptor { 12 | 13 | override fun intercept(chain: Interceptor.Chain): Response { 14 | var request = chain.request() 15 | try { 16 | request = request.newBuilder() 17 | .addHeader("accept-encoding", "gzip, deflate, br") 18 | .addHeader("accept", "text/event-stream") 19 | .addHeader("accept-language", "en-GB,en-US;q=0.9,en;q=0.8") 20 | .addHeader("content-type", "application/json") 21 | .addHeader("referer", "https://chat.openai.com/chat") 22 | .addHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36") 23 | .apply { addHeader("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1UaEVOVUpHTkVNMVFURTRNMEZCTWpkQ05UZzVNRFUxUlRVd1FVSkRNRU13UmtGRVFrRXpSZyJ9.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL3Byb2ZpbGUiOnsiZW1haWwiOiJxYW1hci5lbHNhZmFkaTk4QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJnZW9pcF9jb3VudHJ5IjoiVVMifSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7InVzZXJfaWQiOiJ1c2VyLXFSRzNhSkZTNFAwQUxPak9ETDB1c1M5YiJ9LCJpc3MiOiJodHRwczovL2F1dGgwLm9wZW5haS5jb20vIiwic3ViIjoiYXV0aDB8NjM5NjEyOGZjMzNiYTRmMjIwN2RiNmU3IiwiYXVkIjpbImh0dHBzOi8vYXBpLm9wZW5haS5jb20vdjEiLCJodHRwczovL29wZW5haS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNjcwNzc5NzE5LCJleHAiOjE2NzA4MjI5MTksImF6cCI6IlRkSkljYmUxNldvVEh0Tjk1bnl5d2g1RTR5T282SXRHIiwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSBtb2RlbC5yZWFkIG1vZGVsLnJlcXVlc3Qgb3JnYW5pemF0aW9uLnJlYWQgb2ZmbGluZV9hY2Nlc3MifQ.Y_sVyJu7Izwo5cATJnVJLLSZfgIet4rTBmKuAcZtZ3QJtXOXniEsAoBuZ-rwu79NJEFzTJAU0IDWIbSzo0qprQbZXUp2TRlNd5psGs42T7rt0HrSkKbWZNoXTOrGnnY0R-DSbKOcM9Ig8EYr_Za3scGOjB7vNP4JbZFWTdhI6Gjg4zIX_5Mvo0nBDQpwotOj9SPFj0snu7azt7aZAy_NQNGQgkSvwQOrKr3bqphEaJTIs-1EilecodN1jUpp2oz4CYsmQ0ZVeejnx3Lo-CJiiFJiJYEqqI-zqcMxeWIwdYGBAosodh0VOsrgQmG22fnPEBEVxXgopWAbVrfapYAQcA") } 24 | .build() 25 | 26 | }catch (e:Exception){ 27 | e.printStackTrace() 28 | } 29 | return chain.proceed(request) 30 | 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/remote/retrofit/RetrofitBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.remote.retrofit 2 | 3 | import android.content.Context 4 | import dagger.hilt.android.qualifiers.ApplicationContext 5 | import okhttp3.Authenticator 6 | import okhttp3.Interceptor 7 | import okhttp3.OkHttpClient 8 | import retrofit2.Retrofit 9 | import retrofit2.converter.gson.GsonConverterFactory 10 | import java.util.concurrent.TimeUnit 11 | import javax.inject.Inject 12 | import javax.inject.Singleton 13 | 14 | @Singleton 15 | class RetrofitBuilder @Inject constructor(@ApplicationContext private val context: Context) { 16 | private var okHttpClientBuilder: OkHttpClient.Builder? = null 17 | private var interceptors = mutableListOf() 18 | private var authenticator: Authenticator? = null 19 | private var baseUrl: String = "https://chat.openai.com/backend-api/" 20 | private val TIME_OUT = 80 * 1000L 21 | 22 | fun addInterceptors(vararg interceptor: Interceptor): RetrofitBuilder { 23 | interceptors.addAll(interceptor) 24 | return this 25 | } 26 | 27 | fun build(): Retrofit { 28 | val clientBuilder = okHttpClientBuilder ?: OkHttpClient.Builder().apply { 29 | connectTimeout(TIME_OUT, TimeUnit.MILLISECONDS) 30 | .readTimeout(TIME_OUT, TimeUnit.MILLISECONDS) 31 | .writeTimeout(TIME_OUT, TimeUnit.MILLISECONDS) 32 | val auth: Authenticator? = when { 33 | authenticator != null -> authenticator 34 | else -> null 35 | } 36 | auth?.let { authenticator(it) } 37 | interceptors.forEach { addInterceptor(it) } 38 | } 39 | 40 | return Retrofit.Builder() 41 | .baseUrl(baseUrl) 42 | .client(clientBuilder.build()) 43 | .addConverterFactory(GsonConverterFactory.create()) 44 | .build() 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/remote/service/GptService.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.remote.service 2 | 3 | import com.example.playaroundwithgpt.data.model.DataResponse 4 | import okhttp3.RequestBody 5 | import okhttp3.ResponseBody 6 | import retrofit2.Response 7 | import retrofit2.http.Body 8 | import retrofit2.http.Headers 9 | import retrofit2.http.POST 10 | 11 | interface GptService { 12 | @Headers( "Content-Type: application/json" ) 13 | @POST("conversation") 14 | suspend fun sendMessage( 15 | @Body body: RequestBody 16 | ): Response 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/data/repository/GptRepository.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.data.repository 2 | 3 | import com.example.playaroundwithgpt.data.Resource 4 | import com.example.playaroundwithgpt.data.model.DataResponse 5 | import com.example.playaroundwithgpt.data.model.GPTChatRequest 6 | import com.example.playaroundwithgpt.data.remote.service.GptService 7 | import com.example.playaroundwithgpt.utils.fromJson 8 | import com.example.playaroundwithgpt.utils.handleExceptions 9 | import com.example.playaroundwithgpt.utils.handleSuccess 10 | import com.example.playaroundwithgpt.utils.toJson 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.withContext 14 | import okhttp3.MediaType.Companion.toMediaType 15 | import okhttp3.RequestBody.Companion.toRequestBody 16 | import javax.inject.Inject 17 | 18 | 19 | class GptRepository @Inject constructor( 20 | private val api: GptService 21 | ) { 22 | suspend fun sendMessages( 23 | gptChatRequest: GPTChatRequest 24 | ): Flow> = withContext(Dispatchers.IO) { 25 | val data: Flow> = try { 26 | val json = gptChatRequest.toJson() 27 | val responseBody = ("""$json""".trimIndent()).toRequestBody( 28 | contentType = "application/json".toMediaType() 29 | ) 30 | val response = api.sendMessage(responseBody) 31 | if (response.isSuccessful) { 32 | val body = response.body()?.string() 33 | val chatMessage = 34 | body?.split("\n")?.maxBy { it.length }?.replace("data: ", "") 35 | val gptChatResponse = chatMessage?.fromJson() as DataResponse 36 | handleSuccess( 37 | gptChatResponse.message?.content?.parts?.get(0).toString(), 38 | response.message() 39 | ) 40 | } else { 41 | handleExceptions(response.body().toString()) 42 | } 43 | } catch (e: Exception) { 44 | e.printStackTrace() 45 | handleExceptions(e) 46 | } 47 | data 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/di/NetworkingModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.di 2 | 3 | import com.example.playaroundwithgpt.data.remote.retrofit.HeaderInterceptor 4 | import com.example.playaroundwithgpt.data.remote.retrofit.RetrofitBuilder 5 | import com.example.playaroundwithgpt.data.remote.service.GptService 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import retrofit2.Retrofit 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object NetworkingModule { 16 | 17 | @Provides 18 | @Singleton 19 | fun provideRetrofit( 20 | retrofitBuilder: RetrofitBuilder, 21 | headerInterceptor: HeaderInterceptor 22 | ): Retrofit = 23 | retrofitBuilder 24 | .addInterceptors(headerInterceptor) 25 | .build() 26 | 27 | @Provides 28 | @Singleton 29 | fun provideGPTApi(retrofit: Retrofit): GptService = retrofit.create(GptService::class.java) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.di 2 | 3 | import com.example.playaroundwithgpt.data.remote.service.GptService 4 | import com.example.playaroundwithgpt.data.repository.GptRepository 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object RepositoryModule { 14 | 15 | @Provides 16 | @Singleton 17 | fun providerGptRepository( 18 | apiService: GptService 19 | ): GptRepository { 20 | return GptRepository(apiService) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/ui/GptViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.ui 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.example.playaroundwithgpt.data.Resource 6 | import com.example.playaroundwithgpt.data.Status 7 | import com.example.playaroundwithgpt.data.model.DataResponse 8 | import com.example.playaroundwithgpt.data.model.GPTChatRequest 9 | import com.example.playaroundwithgpt.data.model.GPTContent 10 | import com.example.playaroundwithgpt.data.model.GPTMessage 11 | import com.example.playaroundwithgpt.data.repository.GptRepository 12 | import com.example.playaroundwithgpt.utils.Event 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.flow.MutableSharedFlow 15 | import kotlinx.coroutines.flow.SharedFlow 16 | import kotlinx.coroutines.launch 17 | import java.util.UUID 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class GptViewModel @Inject constructor( 22 | private val repository: GptRepository 23 | ) : ViewModel() { 24 | 25 | private val _response = MutableSharedFlow>>(replay = 0) 26 | val response: SharedFlow>> = _response 27 | 28 | 29 | fun sendMessage(message:String) { 30 | viewModelScope.launch { 31 | _response.emit(Event(Resource.loading(null))) 32 | val request = GPTChatRequest( 33 | messages = listOf( 34 | GPTMessage( 35 | id = UUID.randomUUID().toString(), 36 | content = GPTContent(parts = listOf(message)) 37 | ) 38 | ), 39 | conversation_id = "d7e2b289-67c1-4497-a87b-c693c5ec6822", 40 | parent_message_id = "cbd6bbea-8dd7-426b-883c-a3ae0b956539" 41 | ) 42 | val flowResult = repository.sendMessages(request) 43 | flowResult.collect { result -> 44 | when (result.status) { 45 | Status.SUCCESS -> { 46 | _response.emit( 47 | Event( 48 | Resource.success( 49 | result.data!!, 50 | result.message 51 | ) 52 | ) 53 | ) 54 | } 55 | Status.ERROR -> { 56 | _response.emit( 57 | Event( 58 | Resource.error( 59 | null, 60 | result.message) 61 | ) 62 | ) 63 | } 64 | else -> {} 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/ui/ListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.ui 2 | 3 | import android.graphics.Color 4 | import android.util.Log 5 | import android.view.View 6 | import androidx.databinding.BindingAdapter 7 | import androidx.databinding.ViewDataBinding 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.example.playaroundwithgpt.R 10 | import com.example.playaroundwithgpt.databinding.SimpleListItem1Binding 11 | import com.example.playaroundwithgpt.ui.base.Adapter 12 | 13 | 14 | @BindingAdapter(value = ["list", "onClick"]) 15 | fun initMessagesList( 16 | view: RecyclerView, 17 | list: List = listOf(), 18 | onClick: () -> Unit, 19 | ): Adapter { 20 | var adapter: Adapter? = null 21 | adapter = Adapter( 22 | R.layout.simple_list_item_1, 23 | onClick = { position: Int, item: Any -> 24 | 25 | }, 26 | onBind = { position: Int, item: MessageItem, _: View, bind: ViewDataBinding -> 27 | val binding = bind as SimpleListItem1Binding 28 | with(binding) { 29 | Log.e("QMR","${ item.isAnimte}") 30 | if (position % 2 == 0) { 31 | textView.text = item.message 32 | rootLt.setBackgroundColor(Color.parseColor("#14000000")) 33 | } else { 34 | rootLt.setBackgroundColor(Color.parseColor("#FFFFFF")) 35 | if (item.isAnimte == true) { 36 | textView.setCharacterDelay(50); 37 | textView.animateText(item.message) 38 | } else { 39 | textView.text = item.message 40 | 41 | } 42 | } 43 | } 44 | }) 45 | view.adapter = adapter 46 | adapter.items = list.toMutableList() 47 | return adapter 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.ui 2 | 3 | import android.graphics.Color 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | import androidx.activity.viewModels 9 | import androidx.core.os.bundleOf 10 | import androidx.lifecycle.Lifecycle 11 | import androidx.lifecycle.lifecycleScope 12 | import androidx.lifecycle.repeatOnLifecycle 13 | import com.example.playaroundwithgpt.R 14 | import com.example.playaroundwithgpt.data.Resource 15 | import com.example.playaroundwithgpt.data.Status 16 | import com.example.playaroundwithgpt.databinding.ActivityMainBinding 17 | import com.example.playaroundwithgpt.ui.base.Adapter 18 | import com.example.playaroundwithgpt.utils.Event 19 | import com.example.playaroundwithgpt.utils.onDone 20 | import dagger.hilt.android.AndroidEntryPoint 21 | import kotlinx.coroutines.launch 22 | 23 | @AndroidEntryPoint 24 | class MainActivity : AppCompatActivity() { 25 | lateinit var binding: ActivityMainBinding 26 | lateinit var adapter: Adapter 27 | private val viewModel by viewModels() 28 | var messagesList = mutableListOf() 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | binding = ActivityMainBinding.inflate(layoutInflater) 33 | setContentView(binding.root) 34 | with(binding) { 35 | subscribeObservers() 36 | adapter = initMessagesList(listItem) {} 37 | messageEt.onDone { 38 | if (messageEt.text.toString().isEmpty().not()) { 39 | messagesList.add(MessageItem(messageEt.text.toString(), false)) 40 | messagesList.forEach { 41 | it.isAnimte = false 42 | } 43 | adapter.items = messagesList 44 | viewModel.sendMessage(messageEt.text.toString()) 45 | messageEt.text.clear() 46 | } 47 | } 48 | } 49 | 50 | } 51 | 52 | 53 | private fun subscribeObservers() { 54 | lifecycleScope.launch { 55 | repeatOnLifecycle(Lifecycle.State.STARTED) { 56 | viewModel.response.collect { 57 | handleResponse(it) 58 | } 59 | } 60 | } 61 | } 62 | 63 | private fun handleResponse(it: Event>) { 64 | val resp = it.peekContent() 65 | when (resp.status) { 66 | Status.LOADING -> { 67 | binding.loadingView.visibility = View.VISIBLE 68 | } 69 | 70 | Status.SUCCESS -> { 71 | resp.data.let { message -> 72 | binding.loadingView.visibility = View.GONE 73 | adapter.items.forEach { 74 | it.isAnimte = false 75 | } 76 | messagesList.add(MessageItem(message ?: "", true)) 77 | adapter.items = messagesList 78 | } 79 | } 80 | 81 | Status.ERROR -> { 82 | 83 | } 84 | } 85 | } 86 | 87 | 88 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/ui/MessageItem.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.ui 2 | 3 | data class MessageItem (val message: String, var isAnimte: Boolean? = false){ 4 | 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/ui/Typewriter.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.ui 2 | 3 | import android.content.Context 4 | import android.os.Handler 5 | import android.util.AttributeSet 6 | import android.widget.TextView 7 | import androidx.appcompat.widget.AppCompatTextView 8 | 9 | class Typewriter : AppCompatTextView { 10 | private var mText: CharSequence? = null 11 | private var mIndex = 0 12 | private var mDelay: Long = 500 //Default 500ms delay 13 | 14 | constructor(context: Context?) : super(context!!) {} 15 | constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs) {} 16 | 17 | private val mHandler: Handler = Handler() 18 | private val characterAdder: Runnable = object : Runnable { 19 | override fun run() { 20 | setText(mText!!.subSequence(0, mIndex++)) 21 | if (mIndex <= mText!!.length) { 22 | mHandler.postDelayed(this, mDelay) 23 | } 24 | } 25 | } 26 | 27 | fun animateText(text: CharSequence?) { 28 | mText = text 29 | mIndex = 0 30 | setText("") 31 | mHandler.removeCallbacks(characterAdder) 32 | mHandler.postDelayed(characterAdder, mDelay) 33 | } 34 | 35 | fun setCharacterDelay(millis: Long) { 36 | mDelay = millis 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/ui/base/Adapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.ui.base 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.annotation.LayoutRes 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | import androidx.recyclerview.widget.RecyclerView 10 | import kotlin.properties.Delegates 11 | 12 | 13 | class Adapter constructor( 14 | @LayoutRes private val layoutRes: Int, 15 | val onClick: ((Int, T) -> Unit)?, 16 | private val onBind: (Int, T, View, binding: ViewDataBinding) -> Unit, 17 | ) : RecyclerView.Adapter.ViewHolder>() { 18 | 19 | var items = mutableListOf() 20 | set(value) { 21 | field = value 22 | notifyDataSetChanged() 23 | } 24 | fun addAll(list: MutableList) { 25 | items.addAll(list) 26 | notifyItemRangeInserted(items.size-1,items.size) 27 | } 28 | fun add(item:T) { 29 | items.add(item) 30 | notifyItemInserted(itemCount-1) 31 | } 32 | 33 | fun update(position:Int,item:T) { 34 | items[position] = item 35 | notifyItemChanged(position) 36 | } 37 | 38 | 39 | // This keeps track of the currently selected position 40 | var selectedPosition by Delegates.observable(-1) { _, oldPos, newPos -> 41 | if (newPos in items.indices) { 42 | if(oldPos!=-1) 43 | notifyItemChanged(oldPos,"null") 44 | notifyItemChanged(newPos,"null") 45 | } 46 | } 47 | 48 | 49 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 50 | // don't forget to make your layout resource as a data binding 51 | val layoutInflater = LayoutInflater.from(parent.getContext()) 52 | val binding = DataBindingUtil.inflate( 53 | layoutInflater, 54 | layoutRes, 55 | parent, 56 | false 57 | ) 58 | return ViewHolder(binding) 59 | 60 | } 61 | override fun getItemCount(): Int = items.size 62 | 63 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 64 | 65 | holder.setIsRecyclable(false) 66 | items[position]?.let { item -> 67 | holder.bind(position, item, holder.itemViewBinding) 68 | 69 | holder.itemView.setOnClickListener { 70 | selectedPosition = position 71 | // notifyDataSetChanged() 72 | onClick?.let { it1 -> it1(position, item) } 73 | 74 | } 75 | } 76 | } 77 | 78 | fun delete(dummyList: MutableList) { 79 | dummyList.removeAt(selectedPosition) 80 | items = dummyList 81 | } 82 | 83 | inner class ViewHolder(val itemViewBinding: ViewDataBinding) : RecyclerView.ViewHolder( 84 | itemViewBinding.root 85 | ) { 86 | fun bind(position: Int, item: T, itemViewBinding: ViewDataBinding) = onBind( 87 | position, 88 | item, 89 | itemView, 90 | itemViewBinding 91 | ) 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/utils/Event.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.utils 2 | 3 | //https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150 4 | /** 5 | * Used as a wrapper for data that is exposed via a LiveData that represents an event. 6 | */ 7 | open class Event(private val content: T) { 8 | 9 | var hasBeenHandled = false 10 | private set // Allow external read but not write 11 | 12 | /** 13 | * Returns the content and prevents its use again. 14 | */ 15 | fun getContentIfNotHandled(): T? { 16 | return if (hasBeenHandled) { 17 | null 18 | } else { 19 | hasBeenHandled = true 20 | content 21 | } 22 | } 23 | 24 | /** 25 | * Returns the content, even if it's already been handled. 26 | */ 27 | fun peekContent(): T = content 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/playaroundwithgpt/utils/Extentions.kt: -------------------------------------------------------------------------------- 1 | package com.example.playaroundwithgpt.utils 2 | 3 | import android.view.inputmethod.EditorInfo 4 | import android.widget.EditText 5 | import com.example.playaroundwithgpt.data.Resource 6 | import com.google.gson.Gson 7 | import com.google.gson.reflect.TypeToken 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | 10 | 11 | fun handleExceptions(errorBase: Any): MutableStateFlow> { 12 | return MutableStateFlow>( 13 | Resource.error( 14 | null, 15 | errorBase.toString() 16 | ) 17 | ) 18 | } 19 | 20 | val String.color 21 | get() = androidx.compose.ui.graphics.Color(android.graphics.Color.parseColor(this)) 22 | 23 | 24 | fun handleSuccess(data: E?, message: String? = ""): MutableStateFlow> { 25 | return MutableStateFlow( 26 | Resource.success( 27 | data, 28 | message = message 29 | ) 30 | ) 31 | } 32 | 33 | 34 | fun handleExceptions(errorBase: Exception): MutableStateFlow> { 35 | return MutableStateFlow( 36 | Resource.error( 37 | null, 38 | errorBase.message) 39 | ) 40 | } 41 | 42 | object GsonUtil { 43 | 44 | val gson by lazy { Gson() } 45 | 46 | fun toJson(obj: Any): String { 47 | return gson.toJson(obj) 48 | } 49 | inline fun fromJson(json: String): T { 50 | return gson.fromJsonTypeToken(json) 51 | } 52 | 53 | 54 | } 55 | 56 | inline fun Gson.fromJsonTypeToken(json: String) = 57 | this.fromJson(json, object : TypeToken() {}.type) 58 | 59 | 60 | fun Any.toJson() = GsonUtil.toJson(this) 61 | inline fun String.fromJson() = GsonUtil.fromJson(this) 62 | 63 | 64 | fun EditText.onDone(callback: () -> Unit) { 65 | setOnEditorActionListener { _, actionId, _ -> 66 | if (actionId == EditorInfo.IME_ACTION_SEARCH) { 67 | callback.invoke() 68 | } 69 | false 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 17 | 26 | 31 | 37 | 38 | 39 | 40 | 44 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/simple_list_item_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 18 | 19 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qamarelsafadi/AndroidAppWithGPT/8b0a610ed52d236675e9d2299d9c2e2fd87a9b24/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PlayaroundWithGPT 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |