diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..4bf918a
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..e1eea1d
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 9ec55cd..5df2168 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -18,10 +18,30 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
+ // configure signing
+ signingConfigs {
+ release {
+ storeFile file('key.jks')
+ storePassword 'password'
+ keyAlias 'key'
+ keyPassword 'password'
+ }
+ }
+
+ signingConfigs {
+ myConfig {
+ keyAlias 'key0'
+ keyPassword 'chatgpt'
+ storeFile file('key')
+ storePassword 'chatgpt'
+ }
+ }
+
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ signingConfig signingConfigs.myConfig
}
}
compileOptions {
@@ -45,6 +65,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.preference:preference:1.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
@@ -80,6 +101,12 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:$retrofit_ver"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver"
+ // okhttp
+ implementation 'com.squareup.okhttp3:okhttp:4.9.3'
+ implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
+ // okhttp sse
+ implementation 'com.squareup.okhttp3:okhttp-sse:4.9.3'
+
// ViewPager2
implementation "androidx.viewpager2:viewpager2:1.0.0"
@@ -90,5 +117,8 @@ dependencies {
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.5'
+ // eventbus
+ implementation 'org.greenrobot:eventbus:3.2.0'
+
}
tasks.register("prepareKotlinBuildScriptModel"){}
\ No newline at end of file
diff --git a/app/key b/app/key
new file mode 100644
index 0000000..1af8f05
Binary files /dev/null and b/app/key differ
diff --git a/app/release/app-release.apk b/app/release/app-release.apk
new file mode 100644
index 0000000..58a548b
Binary files /dev/null and b/app/release/app-release.apk differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
new file mode 100644
index 0000000..1667a2c
--- /dev/null
+++ b/app/release/output-metadata.json
@@ -0,0 +1,20 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "com.nohjunh.test",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "versionCode": 1,
+ "versionName": "1.0",
+ "outputFile": "app-release.apk"
+ }
+ ],
+ "elementType": "File"
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 37871c3..71998f8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,7 @@
-
+
+ android:usesCleartextTraffic="true"
+ tools:targetApi="31">
+
+
+
+ android:windowSoftInputMode="adjustResize">
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..08a3b03
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/nohjunh/test/App.kt b/app/src/main/java/com/nohjunh/test/App.kt
index e5f3af6..7b2eb6f 100644
--- a/app/src/main/java/com/nohjunh/test/App.kt
+++ b/app/src/main/java/com/nohjunh/test/App.kt
@@ -6,10 +6,10 @@ import timber.log.Timber
class App : Application() {
- // Context -> Global
init {
instance = this
}
+
companion object {
private var instance : App? = null
fun context() : Context {
@@ -20,7 +20,9 @@ class App : Application() {
// Timber setting
override fun onCreate() {
super.onCreate()
- Timber.plant(Timber.DebugTree())
+ if (BuildConfig.DEBUG) {
+ Timber.plant(Timber.DebugTree())
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/adapter/ContentAdapter.kt b/app/src/main/java/com/nohjunh/test/adapter/ContentAdapter.kt
index 2ec87ad..a7c0242 100644
--- a/app/src/main/java/com/nohjunh/test/adapter/ContentAdapter.kt
+++ b/app/src/main/java/com/nohjunh/test/adapter/ContentAdapter.kt
@@ -1,25 +1,27 @@
package com.nohjunh.test.adapter
-import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.ImageButton
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.nohjunh.test.R
import com.nohjunh.test.database.entity.ContentEntity
+import timber.log.Timber
-class ContentAdapter(val context : Context, private val dataSet : List) : RecyclerView.Adapter() {
+class ContentAdapter : RecyclerView.Adapter() {
+
+ private val dataSet: MutableList = mutableListOf()
companion object {
- private const val Gpt = 1
- private const val User = 2
+ const val Gpt = 1
+ const val User = 2
+ private const val STEAM_PAYLOAD = "steam_payload"
}
interface DelChatLayoutClick {
- fun onLongClick(view : View, position: Int)
+ fun onLongClick(view: View, position: Int)
}
var delChatLayoutClick : DelChatLayoutClick? = null
@@ -47,7 +49,16 @@ class ContentAdapter(val context : Context, private val dataSet : List) {
+ if (payloads.isEmpty()) {
+ onBindViewHolder(holder, position)
+ } else {
+ if (payloads[0].toString() == STEAM_PAYLOAD) {
+ holder.contentTV.text = dataSet[position].content
+ }
+ }
}
override fun getItemCount(): Int {
@@ -62,4 +73,36 @@ class ContentAdapter(val context : Context, private val dataSet : List) {
+ dataSet.clear()
+ dataSet.addAll(it)
+ notifyDataSetChanged()
+ }
+
+ fun getLastContent(): String {
+ return steamBuilder.toString()
+ }
+
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/database/ChatDatabase.kt b/app/src/main/java/com/nohjunh/test/database/ChatDatabase.kt
index 290d1b4..081c1b6 100644
--- a/app/src/main/java/com/nohjunh/test/database/ChatDatabase.kt
+++ b/app/src/main/java/com/nohjunh/test/database/ChatDatabase.kt
@@ -4,10 +4,12 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
import com.nohjunh.test.database.dao.ContentDAO
import com.nohjunh.test.database.entity.ContentEntity
-@Database(entities = [ContentEntity::class], version = 2)
+@Database(entities = [ContentEntity::class], version = 3, exportSchema = false)
abstract class ChatDatabase : RoomDatabase() {
abstract fun contentDAO() : ContentDAO
@@ -25,11 +27,19 @@ abstract class ChatDatabase : RoomDatabase() {
ChatDatabase::class.java,
"chatDatabase"
)
- .fallbackToDestructiveMigration()
+// .fallbackToDestructiveMigration()
+ .addMigrations(MIGRATION_2_3)
.build()
INSTANCE = instance
instance
}
}
+
+ private val MIGRATION_2_3 = object : Migration(2, 3) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("ALTER TABLE ContentTable ADD COLUMN type INTEGER NOT NULL DEFAULT 0")
+ database.execSQL("ALTER TABLE ContentTable ADD COLUMN time INTEGER NOT NULL DEFAULT 0")
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/database/entity/ContentEntity.kt b/app/src/main/java/com/nohjunh/test/database/entity/ContentEntity.kt
index 7c44b78..c4a89f6 100644
--- a/app/src/main/java/com/nohjunh/test/database/entity/ContentEntity.kt
+++ b/app/src/main/java/com/nohjunh/test/database/entity/ContentEntity.kt
@@ -15,4 +15,17 @@ data class ContentEntity(
@ColumnInfo(name = "gptOrUser")
var gptOrUser : Int
-)
\ No newline at end of file
+) {
+ @ColumnInfo(name = "type")
+ var type: Int = 0
+ @ColumnInfo(name = "time")
+ var time: Long = 0
+
+ companion object {
+ const val Gpt = 1
+ const val User = 2
+
+ const val TYPE_CONVERSATION = 0
+ const val TYPE_SYSTEM = 1
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/model/GptR.java b/app/src/main/java/com/nohjunh/test/model/GptR.java
new file mode 100644
index 0000000..2d7dd45
--- /dev/null
+++ b/app/src/main/java/com/nohjunh/test/model/GptR.java
@@ -0,0 +1,158 @@
+package com.nohjunh.test.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class GptR {
+
+ @SerializedName("id")
+ private String id;
+ @SerializedName("object")
+ private String object;
+ @SerializedName("created")
+ private Integer created;
+ @SerializedName("model")
+ private String model;
+ @SerializedName("usage")
+ private UsageDTO usage;
+ @SerializedName("choices")
+ private List choices;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getObject() {
+ return object;
+ }
+
+ public void setObject(String object) {
+ this.object = object;
+ }
+
+ public Integer getCreated() {
+ return created;
+ }
+
+ public void setCreated(Integer created) {
+ this.created = created;
+ }
+
+ public String getModel() {
+ return model;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ public UsageDTO getUsage() {
+ return usage;
+ }
+
+ public void setUsage(UsageDTO usage) {
+ this.usage = usage;
+ }
+
+ public List getChoices() {
+ return choices;
+ }
+
+ public void setChoices(List choices) {
+ this.choices = choices;
+ }
+
+ public static class UsageDTO {
+ @SerializedName("prompt_tokens")
+ private Integer promptTokens;
+ @SerializedName("completion_tokens")
+ private Integer completionTokens;
+ @SerializedName("total_tokens")
+ private Integer totalTokens;
+
+ public Integer getPromptTokens() {
+ return promptTokens;
+ }
+
+ public void setPromptTokens(Integer promptTokens) {
+ this.promptTokens = promptTokens;
+ }
+
+ public Integer getCompletionTokens() {
+ return completionTokens;
+ }
+
+ public void setCompletionTokens(Integer completionTokens) {
+ this.completionTokens = completionTokens;
+ }
+
+ public Integer getTotalTokens() {
+ return totalTokens;
+ }
+
+ public void setTotalTokens(Integer totalTokens) {
+ this.totalTokens = totalTokens;
+ }
+ }
+
+ public static class ChoicesDTO {
+ @SerializedName("message")
+ private MessageDTO message;
+ @SerializedName("finish_reason")
+ private String finishReason;
+ @SerializedName("index")
+ private Integer index;
+
+ public MessageDTO getMessage() {
+ return message;
+ }
+
+ public void setMessage(MessageDTO message) {
+ this.message = message;
+ }
+
+ public String getFinishReason() {
+ return finishReason;
+ }
+
+ public void setFinishReason(String finishReason) {
+ this.finishReason = finishReason;
+ }
+
+ public Integer getIndex() {
+ return index;
+ }
+
+ public void setIndex(Integer index) {
+ this.index = index;
+ }
+
+ public static class MessageDTO {
+ @SerializedName("role")
+ private String role;
+ @SerializedName("content")
+ private String content;
+
+ public String getRole() {
+ return role;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/nohjunh/test/model/GptResponse.kt b/app/src/main/java/com/nohjunh/test/model/GptResponse.kt
deleted file mode 100644
index 398a274..0000000
--- a/app/src/main/java/com/nohjunh/test/model/GptResponse.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.nohjunh.test.model
-
-import com.google.gson.JsonArray
-
-data class GptResponse (
- val choices : JsonArray
-
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/model/SteamRsp.java b/app/src/main/java/com/nohjunh/test/model/SteamRsp.java
new file mode 100644
index 0000000..3e5a9c1
--- /dev/null
+++ b/app/src/main/java/com/nohjunh/test/model/SteamRsp.java
@@ -0,0 +1,105 @@
+package com.nohjunh.test.model;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class SteamRsp {
+
+ @SerializedName("id")
+ private String id;
+ @SerializedName("object")
+ private String object;
+ @SerializedName("created")
+ private long created;
+ @SerializedName("model")
+ private String model;
+ @SerializedName("choices")
+ private List choices;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getObject() {
+ return object;
+ }
+
+ public void setObject(String object) {
+ this.object = object;
+ }
+
+ public long getCreated() {
+ return created;
+ }
+
+ public void setCreated(long created) {
+ this.created = created;
+ }
+
+ public String getModel() {
+ return model;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ public List getChoices() {
+ return choices;
+ }
+
+ public void setChoices(List choices) {
+ this.choices = choices;
+ }
+
+ public static class ChoicesDTO {
+ @SerializedName("delta")
+ private DeltaDTO delta;
+ @SerializedName("index")
+ private int index;
+
+ public DeltaDTO getDelta() {
+ return delta;
+ }
+
+ public void setDelta(DeltaDTO delta) {
+ this.delta = delta;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public void setIndex(int index) {
+ this.index = index;
+ }
+
+ public static class DeltaDTO {
+ @SerializedName("role")
+ private String role;
+ @SerializedName("content")
+ private String content;
+
+ public String getRole() {
+ return role == null ? "" : role;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public String getContent() {
+ return content == null ? "" : content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/nohjunh/test/model/event/EventData.kt b/app/src/main/java/com/nohjunh/test/model/event/EventData.kt
new file mode 100644
index 0000000..b3bc4eb
--- /dev/null
+++ b/app/src/main/java/com/nohjunh/test/model/event/EventData.kt
@@ -0,0 +1,18 @@
+package com.nohjunh.test.model.event
+
+data class SteamDataEvent(
+ val data: String,
+ val type: Int = TYPE_STEAM
+) {
+ companion object {
+ const val TYPE_STEAM = 0
+ const val TYPE_STEAM_START = 1
+ const val TYPE_STEAM_END = 2
+ }
+
+ fun isSteamStart() = type == TYPE_STEAM_START
+
+ fun isSteamEnd() = type == TYPE_STEAM_END
+
+ fun isSteam() = type == TYPE_STEAM
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/network/Apis.kt b/app/src/main/java/com/nohjunh/test/network/Apis.kt
index a8f1806..f220fd4 100644
--- a/app/src/main/java/com/nohjunh/test/network/Apis.kt
+++ b/app/src/main/java/com/nohjunh/test/network/Apis.kt
@@ -1,18 +1,13 @@
package com.nohjunh.test.network
-import com.google.gson.JsonObject
-import com.nohjunh.test.model.GptResponse
+import com.nohjunh.test.model.GptR
+import okhttp3.RequestBody
import retrofit2.http.Body
-import retrofit2.http.Headers
import retrofit2.http.POST
interface Apis {
- @Headers(
- "Content-Type:application/json",
- "Authorization:Bearer API")
- @POST("v1/completions")
- suspend fun postRequest(
- @Body json : JsonObject
- ) : GptResponse
+
+ @POST(RetrofitInstance.CHAT_URL_PATH)
+ suspend fun postRequest(@Body json: RequestBody): GptR
}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/network/RetrofitInstance.kt b/app/src/main/java/com/nohjunh/test/network/RetrofitInstance.kt
index 5e8290d..2111ee1 100644
--- a/app/src/main/java/com/nohjunh/test/network/RetrofitInstance.kt
+++ b/app/src/main/java/com/nohjunh/test/network/RetrofitInstance.kt
@@ -1,8 +1,20 @@
package com.nohjunh.test.network
+import com.google.gson.Gson
+import com.nohjunh.test.BuildConfig
+import com.nohjunh.test.model.SteamRsp
+import com.nohjunh.test.model.event.SteamDataEvent
import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
+import okhttp3.sse.EventSource
+import okhttp3.sse.EventSourceListener
+import okhttp3.sse.EventSources
+import org.greenrobot.eventbus.EventBus
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
+import timber.log.Timber
import java.util.concurrent.TimeUnit
@@ -12,10 +24,34 @@ object RetrofitInstance {
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES)
+ .addInterceptor(HttpLoggingInterceptor {
+ if (BuildConfig.DEBUG) {
+ Timber.d(it)
+ }
+ }.apply {
+ level = HttpLoggingInterceptor.Level.HEADERS
+ })
+ .addInterceptor { chain ->
+ val request = chain.request()
+ val newRequest = request.newBuilder()
+ .addHeader("Content-Type", "application/json")
+ .addHeader("Authorization", "Bearer $token")
+ .build()
+ chain.proceed(newRequest)
+ }
.build()
private const val BASE_URL = "https://api.openai.com/"
+ const val CHAT_URL_PATH = "v1/chat/completions"
+
+ fun getSteamRequest(): Request.Builder {
+ return Request.Builder()
+ .url(BASE_URL + CHAT_URL_PATH)
+ }
+
+ var token = ""
+
private val client = Retrofit
.Builder()
.baseUrl(BASE_URL)
@@ -27,4 +63,44 @@ object RetrofitInstance {
return client
}
+ private val eventFactory by lazy { EventSources.createFactory(okHttpClient) }
+ private val gson by lazy { Gson() }
+
+ private val sseListener by lazy {
+ object : EventSourceListener() {
+ override fun onClosed(eventSource: EventSource) {
+ Timber.d("Event close ${eventSource.request().url}")
+ }
+
+ override fun onEvent(eventSource: EventSource, id: String?, type: String?, data: String) {
+ super.onEvent(eventSource, id, type, data)
+ Timber.d("Event data $data id $id type $type")
+ if (data == "[DONE]") {
+ EventBus.getDefault().post(SteamDataEvent("", SteamDataEvent.TYPE_STEAM_END))
+ } else {
+ val streamRsp = gson.fromJson(data, SteamRsp::class.java)
+ if (streamRsp.choices.isEmpty()) return
+ val delta = streamRsp.choices.first().delta
+ val content = delta.content
+ if (content.isEmpty()) return
+ EventBus.getDefault().post(SteamDataEvent(content, if(delta.role.isEmpty()) SteamDataEvent.TYPE_STEAM else SteamDataEvent.TYPE_STEAM_START))
+ }
+ }
+
+ override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) {
+ super.onFailure(eventSource, t, response)
+ Timber.d("Event fail ${eventSource.request().url} $t $response")
+ }
+
+ override fun onOpen(eventSource: EventSource, response: Response) {
+ super.onOpen(eventSource, response)
+ Timber.d("Event open ${eventSource.request().url}")
+ }
+ }
+ }
+
+ fun sendRequestSteam(request: Request) {
+ eventFactory.newEventSource(request, sseListener)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/repository/DatabaseRepository.kt b/app/src/main/java/com/nohjunh/test/repository/DatabaseRepository.kt
index aece5df..616f9aa 100644
--- a/app/src/main/java/com/nohjunh/test/repository/DatabaseRepository.kt
+++ b/app/src/main/java/com/nohjunh/test/repository/DatabaseRepository.kt
@@ -11,7 +11,14 @@ class DatabaseRepository {
fun getContentData() = database.contentDAO().getContentData()
- fun insertContent(content : String, gptOrUser : Int) = database.contentDAO().insertContent(ContentEntity(0, content, gptOrUser))
+ fun insertContent(content: String, gptOrUser: Int, msgType: Int): ContentEntity {
+ val bean = ContentEntity(0, content, gptOrUser).apply {
+ type = msgType
+ time = System.currentTimeMillis()
+ }
+ database.contentDAO().insertContent(bean)
+ return bean
+ }
fun deleteSelectedContent(id : Int) = database.contentDAO().deleteSelectedContent(id)
diff --git a/app/src/main/java/com/nohjunh/test/repository/NetWorkRepository.kt b/app/src/main/java/com/nohjunh/test/repository/NetWorkRepository.kt
index d46d3a6..75da4cb 100644
--- a/app/src/main/java/com/nohjunh/test/repository/NetWorkRepository.kt
+++ b/app/src/main/java/com/nohjunh/test/repository/NetWorkRepository.kt
@@ -1,13 +1,21 @@
package com.nohjunh.test.repository
-import com.google.gson.JsonObject
import com.nohjunh.test.network.Apis
import com.nohjunh.test.network.RetrofitInstance
-import org.json.JSONObject
+import okhttp3.RequestBody
class NetWorkRepository {
- private val chatGPTClient = RetrofitInstance.getInstance().create(Apis::class.java)
+ private val chatGPTClient by lazy { RetrofitInstance.getInstance().create(Apis::class.java) }
- suspend fun postResponse(jsonData : JsonObject) = chatGPTClient.postRequest(jsonData)
+ suspend fun postResponse(jsonData: RequestBody) = chatGPTClient.postRequest(jsonData)
+
+ fun setToken(token: String) {
+ RetrofitInstance.token = token
+ }
+
+ fun sendSteamRequest(body: RequestBody) {
+ val request = RetrofitInstance.getSteamRequest().post(body).build()
+ RetrofitInstance.sendRequestSteam(request)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/repository/SpRepository.kt b/app/src/main/java/com/nohjunh/test/repository/SpRepository.kt
new file mode 100644
index 0000000..84093c4
--- /dev/null
+++ b/app/src/main/java/com/nohjunh/test/repository/SpRepository.kt
@@ -0,0 +1,40 @@
+package com.nohjunh.test.repository
+
+import android.content.Context
+
+class SpRepository(context: Context) {
+
+ private val sp = context.getSharedPreferences("config", Context.MODE_PRIVATE)
+
+ fun isFirstOpen(): Boolean {
+ return sp.getBoolean("isFirstOpen", true)
+ }
+
+ fun setFirstOpen(isFirstOpen: Boolean) {
+ sp.edit().putBoolean("isFirstOpen", isFirstOpen).apply()
+ }
+
+ fun getGptModel(): String {
+ return sp.getString("gptModel", "gpt-3.5-turbo-0301") ?: "gpt-3.5-turbo-0301"
+ }
+
+ fun setGptModel(gptModel: String) {
+ sp.edit().putString("gptModel", gptModel).apply()
+ }
+
+ fun getToken(): String {
+ return sp.getString("token", "") ?: ""
+ }
+
+ fun setToken(token: String) {
+ sp.edit().putString("token", token).apply()
+ }
+
+ fun getSendBySteam(): Boolean {
+ return sp.getBoolean("sendBySteam", true)
+ }
+
+ fun setSendBySteam(sendBySteam: Boolean) {
+ sp.edit().putBoolean("sendBySteam", sendBySteam).apply()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/view/MainActivity.kt b/app/src/main/java/com/nohjunh/test/view/MainActivity.kt
index 8384fb5..782e774 100644
--- a/app/src/main/java/com/nohjunh/test/view/MainActivity.kt
+++ b/app/src/main/java/com/nohjunh/test/view/MainActivity.kt
@@ -1,13 +1,12 @@
package com.nohjunh.test.view
-import android.content.DialogInterface
import android.os.Build.VERSION.SDK_INT
-import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
-import android.widget.ScrollView
+import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
@@ -15,24 +14,28 @@ import coil.Coil
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
-import coil.load
+import com.google.android.material.textfield.TextInputLayout
+import com.nohjunh.test.BuildConfig
import com.nohjunh.test.R
import com.nohjunh.test.adapter.ContentAdapter
import com.nohjunh.test.database.entity.ContentEntity
import com.nohjunh.test.databinding.ActivityMainBinding
+import com.nohjunh.test.model.event.SteamDataEvent
+import com.nohjunh.test.view.settings.SettingsActivity
import com.nohjunh.test.viewModel.MainViewModel
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
class MainActivity : AppCompatActivity() {
- private var branch : Int = 1 // # 1 -> First time loading
- private lateinit var binding : ActivityMainBinding
- private val viewModel : MainViewModel by viewModels()
- private var contentDataList = ArrayList()
+ private var branch: Int = 1 // # 1 -> First time loading
+ private lateinit var binding: ActivityMainBinding
+ private val viewModel: MainViewModel by viewModels()
+
+ private val layoutManager = LinearLayoutManager(this).apply { stackFromEnd = true }
+ private val adapter = ContentAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -41,9 +44,11 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
+ binding.RVContainer.layoutManager = layoutManager
+ binding.RVContainer.adapter = adapter
+ viewModel.checkToken()
- /* coil GIF 확장 라이브러리 */
- val imageLoader = this?.let {
+ val imageLoader = this.let {
ImageLoader.Builder(it)
.components {
if (SDK_INT >= 28) {
@@ -54,22 +59,14 @@ class MainActivity : AppCompatActivity() {
}
.build()
}
- if (imageLoader != null) {
- Coil.setImageLoader(imageLoader)
- }
+ Coil.setImageLoader(imageLoader)
binding.loading.visibility = View.INVISIBLE
- binding.loading.load(R.drawable.loading3)
-
- // 로딩 되었을 때 바로 content가 보이도록
- viewModel.getContentData()
- viewModel.contentList.observe(this, Observer {
- contentDataList.clear()
- for (entity in it) {
- contentDataList.add(entity)
- }
- setContentListRV(branch)
- })
+ viewModel.contentList.observe(this) {
+ Timber.d("contentList: ${it.size}")
+ adapter.submitList(it)
+ layoutManager.scrollToPosition(adapter.itemCount - 1)
+ }
viewModel.deleteCheck.observe(this, Observer {
if (it == true) {
@@ -86,52 +83,114 @@ class MainActivity : AppCompatActivity() {
branch = 2
})
+ viewModel.showLoading.observe(this) {
+ if (it == true) {
+ binding.loading.visibility = View.VISIBLE
+ } else {
+ binding.loading.visibility = View.INVISIBLE
+ }
+ }
+
+ viewModel.showDeleteGuide.observe(this) {
+// if (it == true) {
+// showAsk(getString(R.string.ask_title_notice), getString(R.string.ask_msg_delete_guide), {
+// viewModel.resetFirstOpen()
+// })
+// }
+ }
+ viewModel.chatGptNewMsg.observe(this) {
+ if (it != null) {
+ adapter.addMsg(it)
+ layoutManager.scrollToPosition(adapter.itemCount - 1)
+ }
+ }
+
binding.sendBtn.setOnClickListener {
binding.loading.visibility = View.VISIBLE
-
- viewModel.postResponse(binding.EDView.text.toString())
- viewModel.insertContent(binding.EDView.text.toString(), 2) // 1: Gpt, 2: User
- binding.EDView.setText("")
+ val msg = binding.EDView.text.toString().trim()
+ if (msg.isEmpty()) {
+ viewModel.insertContent(getString(R.string.chat_tips), ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM) // 1: Gpt, 2: User
+ return@setOnClickListener
+ }
+ if (!BuildConfig.DEBUG) {
+ binding.EDView.setText("")
+ }
branch = 2
- viewModel.getContentData()
+ viewModel.insertContent(msg, ContentEntity.User, ContentEntity.TYPE_CONVERSATION, false) // 1: Gpt, 2: User
+ adapter.addMsg(ContentEntity(0, msg, ContentEntity.User))
+
+ if (viewModel.sendBySteam) {
+ adapter.addChatGpt("waiting for response...")
+ }
+ layoutManager.scrollToPosition(adapter.itemCount - 1)
+ viewModel.postResponse(msg)
}
+ binding.ivSetting.setOnClickListener {
+// showInputDialog()
+ SettingsActivity.start(this)
+ }
+ viewModel.getContentData()
+ if (BuildConfig.DEBUG) {
+ binding.EDView.setText("介绍一下你自己")
+ }
+
+ EventBus.getDefault().register(this)
}
- private fun setContentListRV(branch : Int) {
- val contentAdapter = ContentAdapter(this, contentDataList)
- binding.RVContainer.adapter = contentAdapter
- binding.RVContainer.layoutManager = LinearLayoutManager(this).apply {
- // 맨 밑부터 보이게
- stackFromEnd = true
- }
- // 맨 밑부터 보이게
- //binding.RVContainer.scrollToPosition(contentDataList.size-1)
- CoroutineScope(Dispatchers.Main).launch {
- delay(100)
- binding.SVContainer.fullScroll(ScrollView.FOCUS_DOWN);
- if (branch != 1) {
- binding.EDView.requestFocus()
+ private fun showInputDialog() {
+ val inputView = layoutInflater.inflate(R.layout.input_layout, null)
+
+ val builder = AlertDialog.Builder(this)
+ builder.setTitle(getString(R.string.ask_title_setting))
+ builder.setMessage(getString(R.string.ask_msg_setting))
+ .setCancelable(false)
+ .setView(inputView)
+ builder.setPositiveButton(getString(R.string.ask_ok)) { dialog, which ->
+ val etInput = inputView.findViewById(R.id.etInput)
+ val text = etInput.editText?.text.toString().trim()
+ if (text.isEmpty()) {
+ Toast.makeText(this, getString(R.string.api_key_empty_error), Toast.LENGTH_SHORT).show()
+ return@setPositiveButton
}
+ viewModel.setupApiKey(text)
+ viewModel.insertContent(getString(R.string.api_key_inserted), ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM)
+ }
+ builder.setNegativeButton(getString(R.string.ask_cancel)) { dialog, which ->
+ dialog.dismiss()
}
- // onClick 구현
- contentAdapter.delChatLayoutClick = object : ContentAdapter.DelChatLayoutClick {
- override fun onLongClick(view : View, position: Int) {
- Timber.tag("삭제버튼클릭").e("${contentDataList[position].id}")
- // alertDialog
- val builder = AlertDialog.Builder(this@MainActivity)
- builder.setTitle("대화컨텐츠 삭제")
- .setMessage("한번 삭제된 대화는 복구할 수 없습니다.")
- .setPositiveButton("확인",
- DialogInterface.OnClickListener { dialog, id ->
- viewModel.deleteSelectedContent(contentDataList[position].id)
- })
- .setNegativeButton("취소",
- DialogInterface.OnClickListener { dialog, id ->
- })
- // 다이얼로그를 띄워주기
- builder.show()
+ builder.show()
+ }
+
+ fun showAsk(title: String, message: String, positiveAction: () -> Unit, negativeAction: (() -> Unit)? = null) {
+ val builder = AlertDialog.Builder(this@MainActivity)
+ builder.setCancelable(false)
+ builder.setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(R.string.ask_ok) { _, _ ->
+ positiveAction()
+ }
+ .setNegativeButton(R.string.ask_cancel) { _, _ ->
+ negativeAction?.invoke()
}
+ builder.show()
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun receiveSteamData(data: SteamDataEvent) {
+ Timber.d("receiveSteamData: ${data.data} type ${data.type}")
+ if (data.isSteam()) {
+ adapter.appendLast(data.data)
+ layoutManager.scrollToPosition(adapter.itemCount - 1)
+ } else if (data.isSteamStart()) {
+// adapter.addLast("waiting for response...")
+ } else if (data.isSteamEnd()) {
+ viewModel.insertContent(adapter.getLastContent(), ContentEntity.Gpt, ContentEntity.TYPE_CONVERSATION, false)
}
}
+
+ override fun onDestroy() {
+ super.onDestroy()
+ EventBus.getDefault().unregister(this)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/view/settings/SettingsActivity.kt b/app/src/main/java/com/nohjunh/test/view/settings/SettingsActivity.kt
new file mode 100644
index 0000000..11893da
--- /dev/null
+++ b/app/src/main/java/com/nohjunh/test/view/settings/SettingsActivity.kt
@@ -0,0 +1,30 @@
+package com.nohjunh.test.view.settings
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.nohjunh.test.R
+
+class SettingsActivity : AppCompatActivity() {
+
+ companion object {
+ @JvmStatic
+ fun start(context: Context) {
+ val starter = Intent(context, SettingsFragment::class.java)
+ context.startActivity(starter)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.settings_activity)
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.settings, SettingsFragment.newInstance())
+ .commit()
+ }
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/view/settings/SettingsFragment.kt b/app/src/main/java/com/nohjunh/test/view/settings/SettingsFragment.kt
new file mode 100644
index 0000000..c9e1ef2
--- /dev/null
+++ b/app/src/main/java/com/nohjunh/test/view/settings/SettingsFragment.kt
@@ -0,0 +1,17 @@
+package com.nohjunh.test.view.settings
+
+import android.os.Bundle
+import androidx.preference.PreferenceFragmentCompat
+import com.nohjunh.test.R
+
+class SettingsFragment : PreferenceFragmentCompat() {
+
+ companion object {
+ fun newInstance() = SettingsFragment()
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.root_preferences, rootKey)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nohjunh/test/viewModel/MainViewModel.kt b/app/src/main/java/com/nohjunh/test/viewModel/MainViewModel.kt
index ebf86d4..57ef579 100644
--- a/app/src/main/java/com/nohjunh/test/viewModel/MainViewModel.kt
+++ b/app/src/main/java/com/nohjunh/test/viewModel/MainViewModel.kt
@@ -1,24 +1,35 @@
package com.nohjunh.test.viewModel
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
-import com.google.gson.JsonObject
+import com.nohjunh.test.App
+import com.nohjunh.test.BuildConfig
+import com.nohjunh.test.R
import com.nohjunh.test.database.entity.ContentEntity
-import com.nohjunh.test.model.GptText
+import com.nohjunh.test.model.event.SteamDataEvent
import com.nohjunh.test.repository.DatabaseRepository
import com.nohjunh.test.repository.NetWorkRepository
+import com.nohjunh.test.repository.SpRepository
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import org.json.JSONObject
+import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody.Companion.toRequestBody
+import org.greenrobot.eventbus.EventBus
import timber.log.Timber
+import kotlin.random.Random
-class MainViewModel : ViewModel() {
+class MainViewModel(application: Application) : AndroidViewModel(application) {
private val databaseRepository = DatabaseRepository()
private val netWorkRepository = NetWorkRepository()
+ private val spRepository = SpRepository(application)
private var _contentList = MutableLiveData>()
val contentList : LiveData>
@@ -32,26 +43,90 @@ class MainViewModel : ViewModel() {
val gptInsertCheck : LiveData
get() = _gptInsertCheck
- fun postResponse(query : String) = viewModelScope.launch {
- val jsonObject: JsonObject? = JsonObject().apply{
- // params
- addProperty("model", "text-davinci-003")
- addProperty("prompt", query)
- addProperty("temperature", 0)
- addProperty("max_tokens", 500)
- addProperty("top_p", 1)
- addProperty("frequency_penalty", 0.0)
- addProperty("presence_penalty", 0.0)
+ private var _showLoading = MutableLiveData(false)
+ val showLoading : LiveData
+ get() = _showLoading
+
+ private var _showDeleteGuide = MutableLiveData(false)
+ val showDeleteGuide : LiveData
+ get() = _showDeleteGuide
+
+ private var _chatGptNewMsg = MutableLiveData()
+ val chatGptNewMsg : LiveData
+ get() = _chatGptNewMsg
+
+ private val gson by lazy { Gson() }
+ var sendBySteam = false
+
+ companion object {
+ private const val TAG = "MainViewModel"
+ }
+
+ init {
+ _showDeleteGuide.postValue(spRepository.isFirstOpen())
+ sendBySteam = spRepository.getSendBySteam()
+ }
+
+ fun checkToken() {
+ val token = spRepository.getToken()
+ if (token.isNotEmpty()) {
+ netWorkRepository.setToken(token)
+ } else {
+ viewModelScope.launch {
+ Log.d(TAG, "checkToken: thread: ${Thread.currentThread().name}")
+ withContext(Dispatchers.IO) {
+ _chatGptNewMsg.postValue(databaseRepository.insertContent(getApplication().getString(R.string.api_key_empty_error), ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM))
+ }
+ }
}
- val response = netWorkRepository.postResponse(jsonObject!!)
- Timber.tag("응답결과").e("${response.choices.get(0)}")
- // json -> object 는 fromJson
- // object -> json 은 toJson
- val gson = Gson()
- val tempjson = gson.toJson(response.choices.get(0))
- val tempgson = gson.fromJson(tempjson, GptText::class.java)
- Timber.tag("가공결과").e("${tempgson.text}")
- insertContent(tempgson.text.toString(), 1)
+ }
+
+ fun postResponse(query: String) = viewModelScope.launch {
+ val jsonObj = mutableMapOf()
+
+ jsonObj["model"] = "gpt-3.5-turbo-0301"
+ jsonObj["messages"] = listOf(mapOf("role" to "user", "content" to query))
+ jsonObj["temperature"] = 1
+ jsonObj["max_tokens"] = 1000
+ jsonObj["top_p"] = 1
+ jsonObj["stream"] = sendBySteam
+
+ withContext(Dispatchers.IO) {
+ val body = gson.toJson(jsonObj).toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
+ if (sendBySteam) {
+// if (BuildConfig.DEBUG) {
+// val text = "我是一个能够为您提供智能化语言交互服务的AI助手。我被称为OpenAI GPT-3,可以帮助您回答各种问题、提供不同主题的建议和信息等。"
+// delay(Random.nextLong(1000, 2000))
+// EventBus.getDefault().post(SteamDataEvent("", SteamDataEvent.TYPE_STEAM_START))
+// for (c in text.toCharArray()) {
+// EventBus.getDefault().post(SteamDataEvent(c.toString(), SteamDataEvent.TYPE_STEAM))
+// delay(Random.nextLong(50, 500))
+// }
+// EventBus.getDefault().post(SteamDataEvent("", SteamDataEvent.TYPE_STEAM_END))
+// } else {
+ netWorkRepository.sendSteamRequest(body)
+// }
+ } else {
+ _showLoading.postValue(true)
+ val msg = try {
+ val response = netWorkRepository.postResponse(body)
+ if (response.choices.isEmpty()) {
+ val content = getApplication().getString(R.string.chat_error_no_answers)
+ databaseRepository.insertContent(content, ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM)
+ } else {
+ val content = response.choices.first().message.content
+ databaseRepository.insertContent(content, ContentEntity.Gpt, ContentEntity.TYPE_CONVERSATION)
+ }
+ } catch (e: Exception) {
+ Timber.e(e)
+ val content = getApplication().getString(R.string.chat_error_catch) + e.message
+ databaseRepository.insertContent(content, ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM)
+ }
+ _chatGptNewMsg.postValue(msg)
+ _showLoading.postValue(false)
+ }
+ }
+ _showLoading.postValue(false)
}
fun getContentData() = viewModelScope.launch(Dispatchers.IO) {
@@ -60,16 +135,25 @@ class MainViewModel : ViewModel() {
_gptInsertCheck.postValue(false)
}
- fun insertContent(content : String, gptOrUser : Int) = viewModelScope.launch(Dispatchers.IO) {
- databaseRepository.insertContent(content, gptOrUser)
- if (gptOrUser == 1) {
- _gptInsertCheck.postValue(true)
+ fun insertContent(content: String, gptOrUser: Int, type: Int = ContentEntity.TYPE_CONVERSATION, addToView: Boolean = true) = viewModelScope.launch(Dispatchers.IO) {
+ val insertContent = databaseRepository.insertContent(content, gptOrUser, type)
+ if (addToView) {
+ _chatGptNewMsg.postValue(insertContent)
}
}
- fun deleteSelectedContent(id : Int) = viewModelScope.launch(Dispatchers.IO) {
+ fun deleteSelectedContent(id: Int) = viewModelScope.launch(Dispatchers.IO) {
databaseRepository.deleteSelectedContent(id)
_deleteCheck.postValue(true)
}
+ fun resetFirstOpen() {
+ spRepository.setFirstOpen(false)
+ _showDeleteGuide.postValue(false)
+ }
+
+ fun setupApiKey(text: String) {
+ spRepository.setToken(text)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d1..0000000
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml
new file mode 100644
index 0000000..b240b83
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_settings_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/cornerlayout.xml b/app/src/main/res/drawable/cornerlayout.xml
index 81e1af6..267e25d 100644
--- a/app/src/main/res/drawable/cornerlayout.xml
+++ b/app/src/main/res/drawable/cornerlayout.xml
@@ -3,11 +3,7 @@
android:padding="10dp"
android:shape="rectangle" >
-
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..1a4c602
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 4318d4b..d233f9c 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,6 +1,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintTop_toTopOf="parent" />
-
-
-
-
-
+
-
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/gpt_content_item.xml b/app/src/main/res/layout/gpt_content_item.xml
index c05bbc5..c7b6d57 100644
--- a/app/src/main/res/layout/gpt_content_item.xml
+++ b/app/src/main/res/layout/gpt_content_item.xml
@@ -43,12 +43,13 @@
android:id="@+id/rvItemTV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="TESTTESTTEST"
+ tools:text="ChatGPT Response"
android:textColor="@color/black"
- android:textSize="15dp"
+ android:textSize="15sp"
android:fontFamily="@font/nanumgothicbold"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
+ android:textIsSelectable="true"
android:layout_marginBottom="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/app/src/main/res/layout/input_layout.xml b/app/src/main/res/layout/input_layout.xml
new file mode 100644
index 0000000..fdcedcf
--- /dev/null
+++ b/app/src/main/res/layout/input_layout.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
new file mode 100644
index 0000000..de6591a
--- /dev/null
+++ b/app/src/main/res/layout/settings_activity.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/user_content_item.xml b/app/src/main/res/layout/user_content_item.xml
index f495f64..7cea7d9 100644
--- a/app/src/main/res/layout/user_content_item.xml
+++ b/app/src/main/res/layout/user_content_item.xml
@@ -23,19 +23,20 @@
android:id="@+id/rvItemTV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="TESTTESTTEST"
+ android:textIsSelectable="true"
android:textColor="@color/black"
- android:textSize="15dp"
+ android:textSize="15sp"
android:fontFamily="@font/nanumgothicbold"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
app:layout_constraintStart_toStartOf="parent"
+ tools:text="User Input Message"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index eca70cf..7353dbd 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index eca70cf..7353dbd 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a15df32
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..c2a74d6
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..04fec5f
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..02febac
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..2492b04
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4ec00a6
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15e570a
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..0ab1a2c
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4111434
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..332e77f
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..6cf9ed4
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,12 @@
+
+
+
+ - Reply
+ - Reply to all
+
+
+
+ - reply
+ - reply_all
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..d63f5a8
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #0095E2
+
\ 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 fde7ba5..c0ba22e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,34 @@
- test
+ 快聊
+ 您想说些什么?
+ ChatGPT
+ 确定
+ 取消
+ 删除对话
+ 一旦删除的对话将无法恢复。
+ 提示
+ 长按对话内容可删除
+ API Key
+ 设置
+ 请输入您的 API Key
+ API Key 必须要设置才能使用,请点击右上角设置按钮进行设置。
+ API Key 成功设置,重启后使用。
+ 对不起,ChatGPT 无法回答这个问题
+ 对不起,ChatGPT 遇到问题了,错误信息:
+ 您可以跟我对话,历史、人文、科技、甚至是段子、笑话都可以。
+ SettingsActivity
+
+
+ Messages
+ Sync
+
+
+ Your signature
+ Default reply action
+
+
+ Sync email periodically
+ Download incoming attachments
+ Automatically download attachments for incoming emails
+ Only download attachments when manually requested
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index ceb60e5..e28c171 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -11,6 +11,7 @@
- @color/black
- ?attr/colorPrimaryVariant
+ - ?attr/colorPrimaryVariant
- true
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
new file mode 100644
index 0000000..67158db
--- /dev/null
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/settings.gradle b/settings.gradle
index 901f756..12d311f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -12,5 +12,5 @@ dependencyResolutionManagement {
mavenCentral()
}
}
-rootProject.name = "test"
+rootProject.name = "ChatGPTAndroid"
include ':app'