diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index cac0f150..93b5d15c 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -8,6 +8,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 86042de1..15867d39 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -24,6 +24,10 @@ android {
vectorDrawables {
useSupportLibrary = true
}
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas")
+ }
}
androidResources {
@@ -82,9 +86,6 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.compose.android)
ksp(libs.hilt.compiler)
- // Gemini SDK
- implementation(libs.gemini)
-
// Ktor
implementation(libs.ktor.content.negotiation)
implementation(libs.ktor.core)
@@ -98,6 +99,7 @@ dependencies {
// Markdown
implementation(libs.compose.markdown)
+ implementation(libs.richtext)
// Navigation
implementation(libs.hilt.navigation)
diff --git a/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabase/1.json b/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabase/1.json
new file mode 100644
index 00000000..d75ba192
--- /dev/null
+++ b/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabase/1.json
@@ -0,0 +1,120 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "f1cc616c51cc6e2ff8472c95d5b07c8f",
+ "entities": [
+ {
+ "tableName": "chats",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chat_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `enabled_platform` TEXT NOT NULL, `created_at` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "chat_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabledPlatform",
+ "columnName": "enabled_platform",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "chat_id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "messages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chat_id` INTEGER NOT NULL, `content` TEXT NOT NULL, `image_data` TEXT, `linked_message_id` INTEGER NOT NULL, `platform_type` TEXT, `created_at` INTEGER NOT NULL, FOREIGN KEY(`chat_id`) REFERENCES `chats`(`chat_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "message_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chatId",
+ "columnName": "chat_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageData",
+ "columnName": "image_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "linkedMessageId",
+ "columnName": "linked_message_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "platformType",
+ "columnName": "platform_type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "message_id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "chats",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "chat_id"
+ ],
+ "referencedColumns": [
+ "chat_id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f1cc616c51cc6e2ff8472c95d5b07c8f')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabase/2.json b/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabase/2.json
new file mode 100644
index 00000000..a10e509a
--- /dev/null
+++ b/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabase/2.json
@@ -0,0 +1,130 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "778d15aaf1d9b9853912299330d2ec1e",
+ "entities": [
+ {
+ "tableName": "chats",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chat_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `enabled_platform` TEXT NOT NULL, `created_at` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "chat_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabledPlatform",
+ "columnName": "enabled_platform",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "chat_id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "messages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chat_id` INTEGER NOT NULL, `content` TEXT NOT NULL, `image_data` TEXT, `linked_message_id` INTEGER NOT NULL, `platform_type` TEXT, `created_at` INTEGER NOT NULL, FOREIGN KEY(`chat_id`) REFERENCES `chats`(`chat_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "message_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chatId",
+ "columnName": "chat_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageData",
+ "columnName": "image_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "linkedMessageId",
+ "columnName": "linked_message_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "platformType",
+ "columnName": "platform_type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "message_id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_messages_chat_id",
+ "unique": false,
+ "columnNames": [
+ "chat_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_chat_id` ON `${TABLE_NAME}` (`chat_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "chats",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "chat_id"
+ ],
+ "referencedColumns": [
+ "chat_id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '778d15aaf1d9b9853912299330d2ec1e')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabaseV2/1.json b/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabaseV2/1.json
new file mode 100644
index 00000000..35adfd53
--- /dev/null
+++ b/app/schemas/dev.chungjungsoo.gptmobile.data.database.ChatDatabaseV2/1.json
@@ -0,0 +1,222 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "0767000428c001870ed94d82e9763e4d",
+ "entities": [
+ {
+ "tableName": "chats_v2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`chat_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `enabled_platform` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "chat_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabledPlatform",
+ "columnName": "enabled_platform",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updated_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "chat_id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "messages_v2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chat_id` INTEGER NOT NULL, `content` TEXT NOT NULL, `files` TEXT NOT NULL, `linked_message_id` INTEGER NOT NULL, `platform_type` TEXT, `created_at` INTEGER NOT NULL, FOREIGN KEY(`chat_id`) REFERENCES `chats_v2`(`chat_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "message_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chatId",
+ "columnName": "chat_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "files",
+ "columnName": "files",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkedMessageId",
+ "columnName": "linked_message_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "platformType",
+ "columnName": "platform_type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "created_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "message_id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_messages_v2_chat_id",
+ "unique": false,
+ "columnNames": [
+ "chat_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_v2_chat_id` ON `${TABLE_NAME}` (`chat_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "chats_v2",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "chat_id"
+ ],
+ "referencedColumns": [
+ "chat_id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "platform_v2",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`platform_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT NOT NULL, `name` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `api_url` TEXT NOT NULL, `token` TEXT, `model` TEXT NOT NULL, `temperature` REAL, `top_p` REAL, `system_prompt` TEXT, `stream` INTEGER NOT NULL, `timeout` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "platform_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiUrl",
+ "columnName": "api_url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "model",
+ "columnName": "model",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "temperature",
+ "columnName": "temperature",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "topP",
+ "columnName": "top_p",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "systemPrompt",
+ "columnName": "system_prompt",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "stream",
+ "columnName": "stream",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeout",
+ "columnName": "timeout",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "platform_id"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0767000428c001870ed94d82e9763e4d')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/ChatDatabase.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/ChatDatabase.kt
index b09d72fb..5951c51c 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/ChatDatabase.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/ChatDatabase.kt
@@ -1,5 +1,6 @@
package dev.chungjungsoo.gptmobile.data.database
+import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@@ -9,7 +10,11 @@ import dev.chungjungsoo.gptmobile.data.database.entity.APITypeConverter
import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoom
import dev.chungjungsoo.gptmobile.data.database.entity.Message
-@Database(entities = [ChatRoom::class, Message::class], version = 1)
+@Database(
+ entities = [ChatRoom::class, Message::class],
+ version = 2,
+ autoMigrations = [AutoMigration(from = 1, to = 2)]
+)
@TypeConverters(APITypeConverter::class)
abstract class ChatDatabase : RoomDatabase() {
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/ChatDatabaseV2.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/ChatDatabaseV2.kt
new file mode 100644
index 00000000..5aca80eb
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/ChatDatabaseV2.kt
@@ -0,0 +1,21 @@
+package dev.chungjungsoo.gptmobile.data.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import dev.chungjungsoo.gptmobile.data.database.dao.ChatRoomV2Dao
+import dev.chungjungsoo.gptmobile.data.database.dao.MessageV2Dao
+import dev.chungjungsoo.gptmobile.data.database.dao.PlatformV2Dao
+import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoomV2
+import dev.chungjungsoo.gptmobile.data.database.entity.MessageV2
+import dev.chungjungsoo.gptmobile.data.database.entity.PlatformV2
+import dev.chungjungsoo.gptmobile.data.database.entity.StringListConverter
+
+@Database(entities = [ChatRoomV2::class, MessageV2::class, PlatformV2::class], version = 1, exportSchema = false)
+@TypeConverters(StringListConverter::class)
+abstract class ChatDatabaseV2 : RoomDatabase() {
+
+ abstract fun platformDao(): PlatformV2Dao
+ abstract fun chatRoomDao(): ChatRoomV2Dao
+ abstract fun messageDao(): MessageV2Dao
+}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/ChatRoomV2Dao.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/ChatRoomV2Dao.kt
new file mode 100644
index 00000000..8e3647da
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/ChatRoomV2Dao.kt
@@ -0,0 +1,24 @@
+package dev.chungjungsoo.gptmobile.data.database.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoomV2
+
+@Dao
+interface ChatRoomV2Dao {
+
+ @Query("SELECT * FROM chats_v2 ORDER BY updated_at DESC")
+ suspend fun getChatRooms(): List
+
+ @Insert
+ suspend fun addChatRoom(chatRoom: ChatRoomV2): Long
+
+ @Update
+ suspend fun editChatRoom(chatRoom: ChatRoomV2)
+
+ @Delete
+ suspend fun deleteChatRooms(vararg chatRooms: ChatRoomV2)
+}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/MessageV2Dao.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/MessageV2Dao.kt
new file mode 100644
index 00000000..fd41f8d7
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/MessageV2Dao.kt
@@ -0,0 +1,24 @@
+package dev.chungjungsoo.gptmobile.data.database.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import dev.chungjungsoo.gptmobile.data.database.entity.MessageV2
+
+@Dao
+interface MessageV2Dao {
+
+ @Query("SELECT * FROM messages_v2 WHERE chat_id=:chatInt")
+ suspend fun loadMessages(chatInt: Int): List
+
+ @Insert
+ suspend fun addMessages(vararg messages: MessageV2)
+
+ @Update
+ suspend fun editMessages(vararg message: MessageV2)
+
+ @Delete
+ suspend fun deleteMessages(vararg message: MessageV2)
+}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/PlatformV2Dao.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/PlatformV2Dao.kt
new file mode 100644
index 00000000..26c8c351
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/dao/PlatformV2Dao.kt
@@ -0,0 +1,24 @@
+package dev.chungjungsoo.gptmobile.data.database.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import dev.chungjungsoo.gptmobile.data.database.entity.PlatformV2
+
+@Dao
+interface PlatformV2Dao {
+
+ @Query("SELECT * FROM platform_v2 ORDER BY platform_id ASC")
+ suspend fun getPlatforms(): List
+
+ @Insert
+ suspend fun addPlatform(platform: PlatformV2): Long
+
+ @Update
+ suspend fun editPlatform(platform: PlatformV2)
+
+ @Delete
+ suspend fun deletePlatform(platform: PlatformV2)
+}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/ChatRoomV2.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/ChatRoomV2.kt
new file mode 100644
index 00000000..415a807a
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/ChatRoomV2.kt
@@ -0,0 +1,40 @@
+package dev.chungjungsoo.gptmobile.data.database.entity
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.TypeConverter
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@Entity(tableName = "chats_v2")
+data class ChatRoomV2(
+ /**
+ Now, enabled platforms are stored as list of strings.
+ The strings are UUID V4 strings from PlatformV2.uid
+ */
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "chat_id")
+ val id: Int = 0,
+
+ @ColumnInfo(name = "title")
+ val title: String,
+
+ @ColumnInfo(name = "enabled_platform")
+ val enabledPlatform: List,
+
+ @ColumnInfo(name = "created_at")
+ val createdAt: Long = System.currentTimeMillis() / 1000,
+
+ @ColumnInfo(name = "updated_at")
+ val updatedAt: Long = System.currentTimeMillis() / 1000
+) : Parcelable
+
+class StringListConverter {
+ @TypeConverter
+ fun fromString(value: String): List = value.split(',')
+
+ @TypeConverter
+ fun fromList(value: List): String = value.joinToString(",")
+}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/Message.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/Message.kt
index a29e90e2..50ec1dac 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/Message.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/Message.kt
@@ -3,6 +3,7 @@ package dev.chungjungsoo.gptmobile.data.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
+import androidx.room.Index
import androidx.room.PrimaryKey
import dev.chungjungsoo.gptmobile.data.model.ApiType
@@ -15,7 +16,8 @@ import dev.chungjungsoo.gptmobile.data.model.ApiType
childColumns = ["chat_id"],
onDelete = ForeignKey.CASCADE
)
- ]
+ ],
+ indices = [Index(value = ["chat_id"])]
)
data class Message(
@PrimaryKey(autoGenerate = true)
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/MessageV2.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/MessageV2.kt
new file mode 100644
index 00000000..4479f78a
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/MessageV2.kt
@@ -0,0 +1,46 @@
+package dev.chungjungsoo.gptmobile.data.database.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+
+@Entity(
+ tableName = "messages_v2",
+ foreignKeys = [
+ ForeignKey(
+ entity = ChatRoomV2::class,
+ parentColumns = ["chat_id"],
+ childColumns = ["chat_id"],
+ onDelete = ForeignKey.CASCADE
+ )
+ ],
+ indices = [Index(value = ["chat_id"])]
+)
+data class MessageV2(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo("message_id")
+ val id: Int = 0,
+
+ @ColumnInfo(name = "chat_id")
+ val chatId: Int = 0,
+
+ @ColumnInfo(name = "content")
+ val content: String,
+
+ @ColumnInfo(name = "files")
+ val files: List = listOf(),
+
+ @ColumnInfo(name = "revisions")
+ val revisions: List = listOf(),
+
+ @ColumnInfo(name = "linked_message_id")
+ val linkedMessageId: Int = 0,
+
+ @ColumnInfo(name = "platform_type")
+ val platformType: String?,
+
+ @ColumnInfo(name = "created_at")
+ val createdAt: Long = System.currentTimeMillis() / 1000
+)
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/PlatformV2.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/PlatformV2.kt
new file mode 100644
index 00000000..95d95a18
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/database/entity/PlatformV2.kt
@@ -0,0 +1,49 @@
+package dev.chungjungsoo.gptmobile.data.database.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import java.util.UUID
+
+@Entity(tableName = "platform_v2")
+data class PlatformV2(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo("platform_id")
+ val id: Int = 0,
+
+ @ColumnInfo("uid")
+ val uid: String = UUID.randomUUID().toString(),
+
+ @ColumnInfo("name")
+ val name: String,
+
+ @ColumnInfo(name = "enabled")
+ val enabled: Boolean = false,
+
+ @ColumnInfo(name = "api_url")
+ val apiUrl: String,
+
+ @ColumnInfo(name = "token")
+ val token: String? = null,
+
+ @ColumnInfo(name = "model")
+ val model: String,
+
+ @ColumnInfo(name = "temperature")
+ val temperature: Float? = null,
+
+ @ColumnInfo(name = "top_p")
+ val topP: Float? = null,
+
+ @ColumnInfo(name = "system_prompt")
+ val systemPrompt: String? = null,
+
+ @ColumnInfo(name = "stream")
+ val stream: Boolean = true,
+
+ @ColumnInfo(name = "reasoning")
+ val reasoning: Boolean = false,
+
+ @ColumnInfo(name = "timeout")
+ val timeout: Int = 30
+)
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPIImpl.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPIImpl.kt
index 1eedfc3d..49fb56fb 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPIImpl.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/AnthropicAPIImpl.kt
@@ -5,26 +5,15 @@ import dev.chungjungsoo.gptmobile.data.dto.anthropic.request.MessageRequest
import dev.chungjungsoo.gptmobile.data.dto.anthropic.response.ErrorDetail
import dev.chungjungsoo.gptmobile.data.dto.anthropic.response.ErrorResponseChunk
import dev.chungjungsoo.gptmobile.data.dto.anthropic.response.MessageResponseChunk
-import io.ktor.client.call.body
-import io.ktor.client.request.HttpRequestBuilder
+import io.ktor.client.plugins.sse.sse
import io.ktor.client.request.accept
import io.ktor.client.request.headers
import io.ktor.client.request.setBody
-import io.ktor.client.request.url
-import io.ktor.client.statement.HttpResponse
-import io.ktor.client.statement.HttpStatement
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
-import io.ktor.http.contentType
-import io.ktor.utils.io.ByteReadChannel
-import io.ktor.utils.io.cancel
-import io.ktor.utils.io.readUTF8Line
import javax.inject.Inject
-import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.isActive
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
@@ -43,54 +32,29 @@ class AnthropicAPIImpl @Inject constructor(
this.apiUrl = url
}
- override fun streamChatMessage(messageRequest: MessageRequest): Flow {
- val body = Json.encodeToJsonElement(messageRequest)
-
- val builder = HttpRequestBuilder().apply {
- method = HttpMethod.Post
- if (apiUrl.endsWith("/")) url("${apiUrl}v1/messages") else url("$apiUrl/v1/messages")
- contentType(ContentType.Application.Json)
- setBody(body)
- accept(ContentType.Text.EventStream)
- headers {
- append(API_KEY_HEADER, token ?: "")
- append(VERSION_HEADER, ANTHROPIC_VERSION)
- }
- }
-
- return flow {
- try {
- HttpStatement(builder = builder, client = networkClient()).execute {
- streamEventsFrom(it)
- }
- } catch (e: Exception) {
- emit(ErrorResponseChunk(error = ErrorDetail(type = "network_error", message = e.message ?: "")))
- }
- }
- }
-
- private suspend inline fun FlowCollector.streamEventsFrom(response: HttpResponse) {
- val channel: ByteReadChannel = response.body()
- val jsonInstance = Json { ignoreUnknownKeys = true }
-
+ override fun streamChatMessage(messageRequest: MessageRequest): Flow = flow {
try {
- while (currentCoroutineContext().isActive && !channel.isClosedForRead) {
- val line = channel.readUTF8Line() ?: continue
- val value: T = when {
- line.startsWith(STREAM_END_TOKEN) -> break
- line.startsWith(STREAM_PREFIX) -> jsonInstance.decodeFromString(line.removePrefix(STREAM_PREFIX))
- else -> continue
+ networkClient()
+ .sse(
+ urlString = if (apiUrl.endsWith("/")) "${apiUrl}v1/messages" else "$apiUrl/v1/messages",
+ request = {
+ method = HttpMethod.Post
+ setBody(Json.encodeToJsonElement(messageRequest))
+ accept(ContentType.Text.EventStream)
+ headers {
+ append(API_KEY_HEADER, token ?: "")
+ append(VERSION_HEADER, ANTHROPIC_VERSION)
+ }
+ }
+ ) {
+ incoming.collect { event -> event.data?.let { line -> emit(Json.decodeFromString(line)) } }
}
- emit(value)
- }
- } finally {
- channel.cancel()
+ } catch (e: Exception) {
+ emit(ErrorResponseChunk(error = ErrorDetail(type = "network_error", message = e.message ?: "")))
}
}
companion object {
- private const val STREAM_PREFIX = "data:"
- private const val STREAM_END_TOKEN = "event: message_stop"
private const val API_KEY_HEADER = "x-api-key"
private const val VERSION_HEADER = "anthropic-version"
private const val ANTHROPIC_VERSION = "2023-06-01"
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/NetworkClient.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/NetworkClient.kt
index f51fd5d7..4819e9fc 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/NetworkClient.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/network/NetworkClient.kt
@@ -9,6 +9,7 @@ import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.plugins.sse.SSE
import io.ktor.client.request.header
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
@@ -37,6 +38,8 @@ class NetworkClient @Inject constructor(
)
}
+ install(SSE)
+
install(HttpTimeout) {
requestTimeoutMillis = TIMEOUT.toLong()
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepository.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepository.kt
index bd999ad6..96981fee 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepository.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepository.kt
@@ -1,7 +1,9 @@
package dev.chungjungsoo.gptmobile.data.repository
import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoom
+import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoomV2
import dev.chungjungsoo.gptmobile.data.database.entity.Message
+import dev.chungjungsoo.gptmobile.data.database.entity.MessageV2
import dev.chungjungsoo.gptmobile.data.dto.ApiState
import kotlinx.coroutines.flow.Flow
@@ -9,13 +11,16 @@ interface ChatRepository {
suspend fun completeOpenAIChat(question: Message, history: List): Flow
suspend fun completeAnthropicChat(question: Message, history: List): Flow
- suspend fun completeGoogleChat(question: Message, history: List): Flow
suspend fun completeGroqChat(question: Message, history: List): Flow
suspend fun completeOllamaChat(question: Message, history: List): Flow
suspend fun fetchChatList(): List
+ suspend fun fetchChatListV2(): List
suspend fun fetchMessages(chatId: Int): List
- fun generateDefaultChatTitle(messages: List): String?
- suspend fun updateChatTitle(chatRoom: ChatRoom, title: String)
- suspend fun saveChat(chatRoom: ChatRoom, messages: List): ChatRoom
+ suspend fun fetchMessagesV2(chatId: Int): List
+ suspend fun migrateToChatRoomV2MessageV2()
+ fun generateDefaultChatTitle(messages: List): String?
+ suspend fun updateChatTitle(chatRoom: ChatRoomV2, title: String)
+ suspend fun saveChat(chatRoom: ChatRoomV2, messages: List): ChatRoomV2
suspend fun deleteChats(chatRooms: List)
+ suspend fun deleteChatsV2(chatRooms: List)
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt
index 88ef4ae2..2aad0d20 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/ChatRepositoryImpl.kt
@@ -1,6 +1,5 @@
package dev.chungjungsoo.gptmobile.data.repository
-import android.content.Context
import com.aallam.openai.api.chat.ChatCompletionChunk
import com.aallam.openai.api.chat.ChatCompletionRequest
import com.aallam.openai.api.chat.ChatMessage
@@ -8,19 +7,15 @@ import com.aallam.openai.api.chat.ChatRole
import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIHost
-import com.google.ai.client.generativeai.GenerativeModel
-import com.google.ai.client.generativeai.type.BlockThreshold
-import com.google.ai.client.generativeai.type.Content
-import com.google.ai.client.generativeai.type.GenerateContentResponse
-import com.google.ai.client.generativeai.type.HarmCategory
-import com.google.ai.client.generativeai.type.SafetySetting
-import com.google.ai.client.generativeai.type.content
-import com.google.ai.client.generativeai.type.generationConfig
import dev.chungjungsoo.gptmobile.data.ModelConstants
import dev.chungjungsoo.gptmobile.data.database.dao.ChatRoomDao
+import dev.chungjungsoo.gptmobile.data.database.dao.ChatRoomV2Dao
import dev.chungjungsoo.gptmobile.data.database.dao.MessageDao
+import dev.chungjungsoo.gptmobile.data.database.dao.MessageV2Dao
import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoom
+import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoomV2
import dev.chungjungsoo.gptmobile.data.database.entity.Message
+import dev.chungjungsoo.gptmobile.data.database.entity.MessageV2
import dev.chungjungsoo.gptmobile.data.dto.ApiState
import dev.chungjungsoo.gptmobile.data.dto.anthropic.common.MessageRole
import dev.chungjungsoo.gptmobile.data.dto.anthropic.common.TextContent
@@ -39,15 +34,15 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
class ChatRepositoryImpl @Inject constructor(
- private val appContext: Context,
private val chatRoomDao: ChatRoomDao,
private val messageDao: MessageDao,
+ private val chatRoomV2Dao: ChatRoomV2Dao,
+ private val messageV2Dao: MessageV2Dao,
private val settingRepository: SettingRepository,
private val anthropic: AnthropicAPI
) : ChatRepository {
private lateinit var openAI: OpenAI
- private lateinit var google: GenerativeModel
private lateinit var ollama: OpenAI
private lateinit var groq: OpenAI
@@ -56,12 +51,13 @@ class ChatRepositoryImpl @Inject constructor(
openAI = OpenAI(platform.token ?: "", host = OpenAIHost(baseUrl = platform.apiUrl))
val generatedMessages = messageToOpenAICompatibleMessage(ApiType.OPENAI, history + listOf(question))
+ val prompt = platform.systemPrompt ?: ModelConstants.OPENAI_PROMPT
val generatedMessageWithPrompt = listOf(
- ChatMessage(role = ChatRole.System, content = platform.systemPrompt ?: ModelConstants.OPENAI_PROMPT)
+ ChatMessage(role = ChatRole.System, content = prompt)
) + generatedMessages
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(platform.model ?: ""),
- messages = generatedMessageWithPrompt,
+ messages = if (prompt.isEmpty()) generatedMessages else generatedMessageWithPrompt, // disable system prompt only if user set it to empty explicitly
temperature = platform.temperature?.toDouble(),
topP = platform.topP?.toDouble()
)
@@ -79,11 +75,12 @@ class ChatRepositoryImpl @Inject constructor(
anthropic.setAPIUrl(platform.apiUrl)
val generatedMessages = messageToAnthropicMessage(history + listOf(question))
+ val prompt = platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT
val messageRequest = MessageRequest(
model = platform.model ?: "",
messages = generatedMessages,
maxTokens = ModelConstants.ANTHROPIC_MAXIMUM_TOKEN,
- systemPrompt = platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT,
+ systemPrompt = if (prompt.isEmpty()) null else prompt,
stream = true,
temperature = platform.temperature,
topP = platform.topP
@@ -102,44 +99,18 @@ class ChatRepositoryImpl @Inject constructor(
.onCompletion { emit(ApiState.Done) }
}
- override suspend fun completeGoogleChat(question: Message, history: List): Flow {
- val platform = checkNotNull(settingRepository.fetchPlatforms().firstOrNull { it.name == ApiType.GOOGLE })
- val config = generationConfig {
- temperature = platform.temperature
- topP = platform.topP
- }
- google = GenerativeModel(
- modelName = platform.model ?: "",
- apiKey = platform.token ?: "",
- systemInstruction = content { text(platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT) },
- generationConfig = config,
- safetySettings = listOf(
- SafetySetting(HarmCategory.DANGEROUS_CONTENT, BlockThreshold.ONLY_HIGH),
- SafetySetting(HarmCategory.SEXUALLY_EXPLICIT, BlockThreshold.NONE)
- )
- )
-
- val inputContent = messageToGoogleMessage(history)
- val chat = google.startChat(history = inputContent)
-
- return chat.sendMessageStream(question.content)
- .map { response -> ApiState.Success(response.text ?: "") }
- .catch { throwable -> emit(ApiState.Error(throwable.message ?: "Unknown error")) }
- .onStart { emit(ApiState.Loading) }
- .onCompletion { emit(ApiState.Done) }
- }
-
override suspend fun completeGroqChat(question: Message, history: List): Flow {
val platform = checkNotNull(settingRepository.fetchPlatforms().firstOrNull { it.name == ApiType.GROQ })
groq = OpenAI(platform.token ?: "", host = OpenAIHost(baseUrl = platform.apiUrl))
val generatedMessages = messageToOpenAICompatibleMessage(ApiType.GROQ, history + listOf(question))
+ val prompt = platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT
val generatedMessageWithPrompt = listOf(
- ChatMessage(role = ChatRole.System, content = platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT)
+ ChatMessage(role = ChatRole.System, content = prompt)
) + generatedMessages
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(platform.model ?: ""),
- messages = generatedMessageWithPrompt,
+ messages = if (prompt.isEmpty()) generatedMessages else generatedMessageWithPrompt,
temperature = platform.temperature?.toDouble(),
topP = platform.topP?.toDouble()
)
@@ -156,12 +127,13 @@ class ChatRepositoryImpl @Inject constructor(
ollama = OpenAI(platform.token ?: "", host = OpenAIHost(baseUrl = "${platform.apiUrl}v1/"))
val generatedMessages = messageToOpenAICompatibleMessage(ApiType.OLLAMA, history + listOf(question))
+ val prompt = platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT
val generatedMessageWithPrompt = listOf(
- ChatMessage(role = ChatRole.System, content = platform.systemPrompt ?: ModelConstants.DEFAULT_PROMPT)
+ ChatMessage(role = ChatRole.System, content = prompt)
) + generatedMessages
val chatCompletionRequest = ChatCompletionRequest(
model = ModelId(platform.model ?: ""),
- messages = generatedMessageWithPrompt,
+ messages = if (prompt.isEmpty()) generatedMessages else generatedMessageWithPrompt,
temperature = platform.temperature?.toDouble(),
topP = platform.topP?.toDouble()
)
@@ -175,20 +147,70 @@ class ChatRepositoryImpl @Inject constructor(
override suspend fun fetchChatList(): List = chatRoomDao.getChatRooms()
+ override suspend fun fetchChatListV2(): List = chatRoomV2Dao.getChatRooms()
+
override suspend fun fetchMessages(chatId: Int): List = messageDao.loadMessages(chatId)
- override fun generateDefaultChatTitle(messages: List): String? = messages.sortedBy { it.createdAt }.firstOrNull { it.platformType == null }?.content?.replace('\n', ' ')?.take(50)
+ override suspend fun fetchMessagesV2(chatId: Int): List = messageV2Dao.loadMessages(chatId)
+
+ override suspend fun migrateToChatRoomV2MessageV2() {
+ val leftOverChatRoomV2s = chatRoomV2Dao.getChatRooms()
+ chatRoomV2Dao.deleteChatRooms(*leftOverChatRoomV2s.toTypedArray())
+
+ val chatList = fetchChatList()
+ val platforms = settingRepository.fetchPlatformV2s()
+ val apiTypeMap = mutableMapOf()
+
+ platforms.forEach { platform ->
+ when (platform.name) {
+ "OpenAI" -> apiTypeMap[ApiType.OPENAI] = platform.uid
+ "Anthropic" -> apiTypeMap[ApiType.ANTHROPIC] = platform.uid
+ "Google" -> apiTypeMap[ApiType.GOOGLE] = platform.uid
+ "Groq" -> apiTypeMap[ApiType.GROQ] = platform.uid
+ "Ollama" -> apiTypeMap[ApiType.OLLAMA] = platform.uid
+ }
+ }
+
+ chatList.forEach { chatRoom ->
+ val messages = messageDao.loadMessages(chatRoom.id).map { m ->
+ MessageV2(
+ id = m.id,
+ chatId = m.chatId,
+ content = m.content,
+ files = listOf(),
+ revisions = listOf(),
+ linkedMessageId = m.linkedMessageId,
+ platformType = m.platformType?.let { apiTypeMap[it] },
+ createdAt = m.createdAt
+ )
+ }
- override suspend fun updateChatTitle(chatRoom: ChatRoom, title: String) {
- chatRoomDao.editChatRoom(chatRoom.copy(title = title.replace('\n', ' ').take(50)))
+ chatRoomV2Dao.addChatRoom(
+ ChatRoomV2(
+ id = chatRoom.id,
+ title = chatRoom.title,
+ enabledPlatform = chatRoom.enabledPlatform.map { apiTypeMap[it] ?: "" },
+ createdAt = chatRoom.createdAt,
+ updatedAt = chatRoom.createdAt
+ )
+ )
+
+ messageV2Dao.addMessages(*messages.toTypedArray())
+ }
+ }
+
+ override fun generateDefaultChatTitle(messages: List): String? = messages.sortedBy { it.createdAt }.firstOrNull { it.platformType == null }?.content?.replace('\n', ' ')?.take(50)
+
+ override suspend fun updateChatTitle(chatRoom: ChatRoomV2, title: String) {
+ chatRoomV2Dao.editChatRoom(chatRoom.copy(title = title.replace('\n', ' ').take(50)))
}
- override suspend fun saveChat(chatRoom: ChatRoom, messages: List): ChatRoom {
+ override suspend fun saveChat(chatRoom: ChatRoomV2, messages: List): ChatRoomV2 {
if (chatRoom.id == 0) {
// New Chat
- val chatId = chatRoomDao.addChatRoom(chatRoom)
+ val chatId = chatRoomV2Dao.addChatRoom(chatRoom)
val updatedMessages = messages.map { it.copy(chatId = chatId.toInt()) }
- messageDao.addMessages(*updatedMessages.toTypedArray())
+ messageV2Dao.addMessages(*updatedMessages.toTypedArray())
val savedChatRoom = chatRoom.copy(id = chatId.toInt())
updateChatTitle(savedChatRoom, updatedMessages[0].content)
@@ -196,7 +218,7 @@ class ChatRepositoryImpl @Inject constructor(
return savedChatRoom.copy(title = updatedMessages[0].content.replace('\n', ' ').take(50))
}
- val savedMessages = fetchMessages(chatRoom.id)
+ val savedMessages = fetchMessagesV2(chatRoom.id)
val updatedMessages = messages.map { it.copy(chatId = chatRoom.id) }
val shouldBeDeleted = savedMessages.filter { m ->
@@ -209,10 +231,10 @@ class ChatRepositoryImpl @Inject constructor(
savedMessages.firstOrNull { it.id == m.id } == null
}
- chatRoomDao.editChatRoom(chatRoom)
- messageDao.deleteMessages(*shouldBeDeleted.toTypedArray())
- messageDao.editMessages(*shouldBeUpdated.toTypedArray())
- messageDao.addMessages(*shouldBeAdded.toTypedArray())
+ chatRoomV2Dao.editChatRoom(chatRoom)
+ messageV2Dao.deleteMessages(*shouldBeDeleted.toTypedArray())
+ messageV2Dao.editMessages(*shouldBeUpdated.toTypedArray())
+ messageV2Dao.addMessages(*shouldBeAdded.toTypedArray())
return chatRoom
}
@@ -221,6 +243,10 @@ class ChatRepositoryImpl @Inject constructor(
chatRoomDao.deleteChatRooms(*chatRooms.toTypedArray())
}
+ override suspend fun deleteChatsV2(chatRooms: List) {
+ chatRoomV2Dao.deleteChatRooms(*chatRooms.toTypedArray())
+ }
+
private fun messageToOpenAICompatibleMessage(apiType: ApiType, messages: List): List {
val result = mutableListOf()
@@ -270,20 +296,4 @@ class ChatRepositoryImpl @Inject constructor(
return result
}
-
- private fun messageToGoogleMessage(messages: List): List {
- val result = mutableListOf()
-
- messages.forEach { message ->
- when (message.platformType) {
- null -> result.add(content(role = "user") { text(message.content) })
-
- ApiType.GOOGLE -> result.add(content(role = "model") { text(message.content) })
-
- else -> {}
- }
- }
-
- return result
- }
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepository.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepository.kt
index 48c0a10e..a7ed6765 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepository.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepository.kt
@@ -1,11 +1,14 @@
package dev.chungjungsoo.gptmobile.data.repository
+import dev.chungjungsoo.gptmobile.data.database.entity.PlatformV2
import dev.chungjungsoo.gptmobile.data.dto.Platform
import dev.chungjungsoo.gptmobile.data.dto.ThemeSetting
interface SettingRepository {
suspend fun fetchPlatforms(): List
+ suspend fun fetchPlatformV2s(): List
suspend fun fetchThemes(): ThemeSetting
+ suspend fun migrateToPlatformV2()
suspend fun updatePlatforms(platforms: List)
suspend fun updateThemes(themeSetting: ThemeSetting)
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepositoryImpl.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepositoryImpl.kt
index 49d61aef..265e1124 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepositoryImpl.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/data/repository/SettingRepositoryImpl.kt
@@ -1,6 +1,8 @@
package dev.chungjungsoo.gptmobile.data.repository
import dev.chungjungsoo.gptmobile.data.ModelConstants
+import dev.chungjungsoo.gptmobile.data.database.dao.PlatformV2Dao
+import dev.chungjungsoo.gptmobile.data.database.entity.PlatformV2
import dev.chungjungsoo.gptmobile.data.datastore.SettingDataSource
import dev.chungjungsoo.gptmobile.data.dto.Platform
import dev.chungjungsoo.gptmobile.data.dto.ThemeSetting
@@ -10,7 +12,8 @@ import dev.chungjungsoo.gptmobile.data.model.ThemeMode
import javax.inject.Inject
class SettingRepositoryImpl @Inject constructor(
- private val settingDataSource: SettingDataSource
+ private val settingDataSource: SettingDataSource,
+ private val platformV2Dao: PlatformV2Dao
) : SettingRepository {
override suspend fun fetchPlatforms(): List = ApiType.entries.map { apiType ->
@@ -46,11 +49,43 @@ class SettingRepositoryImpl @Inject constructor(
)
}
+ override suspend fun fetchPlatformV2s(): List = platformV2Dao.getPlatforms()
+
override suspend fun fetchThemes(): ThemeSetting = ThemeSetting(
dynamicTheme = settingDataSource.getDynamicTheme() ?: DynamicTheme.OFF,
themeMode = settingDataSource.getThemeMode() ?: ThemeMode.SYSTEM
)
+ override suspend fun migrateToPlatformV2() {
+ val leftOverPlatformV2s = fetchPlatformV2s()
+ leftOverPlatformV2s.forEach { platformV2Dao.deletePlatform(it) }
+
+ val platforms = fetchPlatforms()
+
+ platforms.forEach { platform ->
+ platformV2Dao.addPlatform(
+ PlatformV2(
+ name = when (platform.name) {
+ ApiType.OPENAI -> "OpenAI"
+ ApiType.ANTHROPIC -> "Anthropic"
+ ApiType.GOOGLE -> "Google"
+ ApiType.GROQ -> "Groq"
+ ApiType.OLLAMA -> "Ollama"
+ },
+ enabled = platform.enabled,
+ apiUrl = platform.apiUrl,
+ token = platform.token,
+ model = platform.model ?: "",
+ temperature = platform.temperature,
+ topP = platform.topP,
+ systemPrompt = platform.systemPrompt,
+ stream = true,
+ reasoning = false
+ )
+ )
+ }
+ }
+
override suspend fun updatePlatforms(platforms: List) {
platforms.forEach { platform ->
settingDataSource.updateStatus(platform.name, platform.enabled)
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/ChatRepositoryModule.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/ChatRepositoryModule.kt
index dc7f1222..fbd2fece 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/ChatRepositoryModule.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/ChatRepositoryModule.kt
@@ -1,13 +1,13 @@
package dev.chungjungsoo.gptmobile.di
-import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dev.chungjungsoo.gptmobile.data.database.dao.ChatRoomDao
+import dev.chungjungsoo.gptmobile.data.database.dao.ChatRoomV2Dao
import dev.chungjungsoo.gptmobile.data.database.dao.MessageDao
+import dev.chungjungsoo.gptmobile.data.database.dao.MessageV2Dao
import dev.chungjungsoo.gptmobile.data.network.AnthropicAPI
import dev.chungjungsoo.gptmobile.data.repository.ChatRepository
import dev.chungjungsoo.gptmobile.data.repository.ChatRepositoryImpl
@@ -21,10 +21,18 @@ object ChatRepositoryModule {
@Provides
@Singleton
fun provideChatRepository(
- @ApplicationContext appContext: Context,
chatRoomDao: ChatRoomDao,
messageDao: MessageDao,
+ chatRoomV2Dao: ChatRoomV2Dao,
+ messageV2Dao: MessageV2Dao,
settingRepository: SettingRepository,
anthropicAPI: AnthropicAPI
- ): ChatRepository = ChatRepositoryImpl(appContext, chatRoomDao, messageDao, settingRepository, anthropicAPI)
+ ): ChatRepository = ChatRepositoryImpl(
+ chatRoomDao,
+ messageDao,
+ chatRoomV2Dao,
+ messageV2Dao,
+ settingRepository,
+ anthropicAPI
+ )
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/DatabaseModule.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/DatabaseModule.kt
index 55f25ef9..f3b8da27 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/DatabaseModule.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/DatabaseModule.kt
@@ -8,21 +8,35 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dev.chungjungsoo.gptmobile.data.database.ChatDatabase
+import dev.chungjungsoo.gptmobile.data.database.ChatDatabaseV2
import dev.chungjungsoo.gptmobile.data.database.dao.ChatRoomDao
+import dev.chungjungsoo.gptmobile.data.database.dao.ChatRoomV2Dao
import dev.chungjungsoo.gptmobile.data.database.dao.MessageDao
+import dev.chungjungsoo.gptmobile.data.database.dao.MessageV2Dao
+import dev.chungjungsoo.gptmobile.data.database.dao.PlatformV2Dao
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
private const val DB_NAME = "chat"
+ private const val DB_NAME_V2 = "chat_v2"
+
+ @Provides
+ fun providePlatformV2Dao(chatDatabaseV2: ChatDatabaseV2): PlatformV2Dao = chatDatabaseV2.platformDao()
@Provides
fun provideChatRoomDao(chatDatabase: ChatDatabase): ChatRoomDao = chatDatabase.chatRoomDao()
+ @Provides
+ fun provideChatRoomV2Dao(chatDatabaseV2: ChatDatabaseV2): ChatRoomV2Dao = chatDatabaseV2.chatRoomDao()
+
@Provides
fun provideMessageDao(chatDatabase: ChatDatabase): MessageDao = chatDatabase.messageDao()
+ @Provides
+ fun provideMessageV2Dao(chatDatabaseV2: ChatDatabaseV2): MessageV2Dao = chatDatabaseV2.messageDao()
+
@Provides
@Singleton
fun provideChatDatabase(@ApplicationContext appContext: Context): ChatDatabase = Room.databaseBuilder(
@@ -30,4 +44,12 @@ object DatabaseModule {
ChatDatabase::class.java,
DB_NAME
).build()
+
+ @Provides
+ @Singleton
+ fun provideChatDatabaseV2(@ApplicationContext appContext: Context): ChatDatabaseV2 = Room.databaseBuilder(
+ appContext,
+ ChatDatabaseV2::class.java,
+ DB_NAME_V2
+ ).build()
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/SettingRepositoryModule.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/SettingRepositoryModule.kt
index 0aab1d04..1b40049b 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/SettingRepositoryModule.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/di/SettingRepositoryModule.kt
@@ -4,6 +4,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import dev.chungjungsoo.gptmobile.data.database.dao.PlatformV2Dao
import dev.chungjungsoo.gptmobile.data.datastore.SettingDataSource
import dev.chungjungsoo.gptmobile.data.repository.SettingRepository
import dev.chungjungsoo.gptmobile.data.repository.SettingRepositoryImpl
@@ -16,6 +17,7 @@ object SettingRepositoryModule {
@Provides
@Singleton
fun provideSettingRepository(
- settingDataSource: SettingDataSource
- ): SettingRepository = SettingRepositoryImpl(settingDataSource)
+ settingDataSource: SettingDataSource,
+ platformV2Dao: PlatformV2Dao
+ ): SettingRepository = SettingRepositoryImpl(settingDataSource, platformV2Dao)
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/NavigationGraph.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/NavigationGraph.kt
index 242e3093..803de053 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/NavigationGraph.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/NavigationGraph.kt
@@ -17,6 +17,7 @@ import androidx.navigation.navigation
import dev.chungjungsoo.gptmobile.data.model.ApiType
import dev.chungjungsoo.gptmobile.presentation.ui.chat.ChatScreen
import dev.chungjungsoo.gptmobile.presentation.ui.home.HomeScreen
+import dev.chungjungsoo.gptmobile.presentation.ui.migrate.MigrateScreen
import dev.chungjungsoo.gptmobile.presentation.ui.setting.AboutScreen
import dev.chungjungsoo.gptmobile.presentation.ui.setting.LicenseScreen
import dev.chungjungsoo.gptmobile.presentation.ui.setting.PlatformSettingScreen
@@ -40,6 +41,7 @@ fun SetupNavGraph(navController: NavHostController) {
startDestination = Route.CHAT_LIST
) {
homeScreenNavigation(navController)
+ migrationScreenNavigation(navController)
startScreenNavigation(navController)
setupNavigation(navController)
settingNavigation(navController)
@@ -47,6 +49,16 @@ fun SetupNavGraph(navController: NavHostController) {
}
}
+fun NavGraphBuilder.migrationScreenNavigation(navController: NavHostController) {
+ composable(Route.MIGRATE_V2) {
+ MigrateScreen {
+ navController.navigate(Route.CHAT_LIST) {
+ popUpTo(Route.MIGRATE_V2) { inclusive = true }
+ }
+ }
+ }
+}
+
fun NavGraphBuilder.startScreenNavigation(navController: NavHostController) {
composable(Route.GET_STARTED) {
StartScreen { navController.navigate(Route.SETUP_ROUTE) }
@@ -179,7 +191,7 @@ fun NavGraphBuilder.homeScreenNavigation(navController: NavHostController) {
HomeScreen(
settingOnClick = { navController.navigate(Route.SETTING_ROUTE) { launchSingleTop = true } },
onExistingChatClick = { chatRoom ->
- val enabledPlatformString = chatRoom.enabledPlatform.joinToString(",") { v -> v.name }
+ val enabledPlatformString = chatRoom.enabledPlatform.joinToString(",")
navController.navigate(
Route.CHAT_ROOM
.replace(oldValue = "{chatRoomId}", newValue = "${chatRoom.id}")
@@ -187,7 +199,7 @@ fun NavGraphBuilder.homeScreenNavigation(navController: NavHostController) {
)
},
navigateToNewChat = {
- val enabledPlatformString = it.joinToString(",") { v -> v.name }
+ val enabledPlatformString = it.joinToString(",")
navController.navigate(
Route.CHAT_ROOM
.replace(oldValue = "{chatRoomId}", newValue = "0")
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/PlatformCheckBoxItem.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/PlatformCheckBoxItem.kt
index 0eebb664..7fcdcb35 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/PlatformCheckBoxItem.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/PlatformCheckBoxItem.kt
@@ -18,16 +18,15 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.chungjungsoo.gptmobile.R
-import dev.chungjungsoo.gptmobile.data.dto.Platform
@Composable
fun PlatformCheckBoxItem(
modifier: Modifier = Modifier,
- platform: Platform,
+ selected: Boolean,
enabled: Boolean = true,
title: String = stringResource(R.string.sample_item_title),
description: String? = stringResource(R.string.sample_item_description),
- onClickEvent: (Platform) -> Unit
+ onClickEvent: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val rowModifier = if (enabled) {
@@ -36,7 +35,7 @@ fun PlatformCheckBoxItem(
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current
- ) { onClickEvent.invoke(platform) }
+ ) { onClickEvent.invoke() }
.padding(top = 12.dp, bottom = 12.dp, start = 16.dp, end = 16.dp)
} else {
modifier
@@ -51,9 +50,9 @@ fun PlatformCheckBoxItem(
) {
Checkbox(
enabled = enabled,
- checked = platform.selected,
+ checked = selected,
interactionSource = interactionSource,
- onCheckedChange = { onClickEvent.invoke(platform) }
+ onCheckedChange = { onClickEvent.invoke() }
)
Column(horizontalAlignment = Alignment.Start) {
Text(
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/Route.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/Route.kt
index 950c5785..97236562 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/Route.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/common/Route.kt
@@ -27,4 +27,6 @@ object Route {
const val OLLAMA_SETTINGS = "ollama_settings"
const val ABOUT_PAGE = "about"
const val LICENSE = "license"
+
+ const val MIGRATE_V2 = "migrate_v2"
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Block.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Block.kt
new file mode 100644
index 00000000..50c06945
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Block.kt
@@ -0,0 +1,70 @@
+package dev.chungjungsoo.gptmobile.presentation.icons
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val Block: ImageVector
+ @Composable
+ get() = Builder(
+ name = "Blocked Icon",
+ defaultWidth = 24.0.dp,
+ defaultHeight = 24.0.dp,
+ viewportWidth = 960.0f,
+ viewportHeight = 960.0f
+ ).apply {
+ path(
+ fill = SolidColor(MaterialTheme.colorScheme.outline),
+ stroke = null,
+ strokeLineWidth = 0.0f,
+ strokeLineCap = Butt,
+ strokeLineJoin = Miter,
+ strokeLineMiter = 4.0f,
+ pathFillType = NonZero
+ ) {
+ moveTo(480.0f, 888.13f)
+ quadToRelative(-84.67f, 0.0f, -159.11f, -32.22f)
+ quadToRelative(-74.43f, -32.21f, -129.63f, -87.41f)
+ quadToRelative(-55.19f, -55.2f, -87.29f, -129.75f)
+ quadToRelative(-32.1f, -74.55f, -32.1f, -159.23f)
+ quadToRelative(0.0f, -84.67f, 32.1f, -158.99f)
+ quadToRelative(32.1f, -74.31f, 87.29f, -129.39f)
+ quadToRelative(55.2f, -55.07f, 129.63f, -87.17f)
+ quadToRelative(74.44f, -32.1f, 159.11f, -32.1f)
+ quadToRelative(84.67f, 0.0f, 159.11f, 32.1f)
+ quadToRelative(74.43f, 32.1f, 129.63f, 87.17f)
+ quadToRelative(55.19f, 55.08f, 87.29f, 129.39f)
+ quadToRelative(32.1f, 74.32f, 32.1f, 158.99f)
+ quadToRelative(0.0f, 84.68f, -32.1f, 159.23f)
+ quadToRelative(-32.1f, 74.55f, -87.29f, 129.75f)
+ quadToRelative(-55.2f, 55.2f, -129.63f, 87.41f)
+ quadTo(564.67f, 888.13f, 480.0f, 888.13f)
+ close()
+ moveTo(480.0f, 797.13f)
+ quadToRelative(52.09f, 0.0f, 100.65f, -16.3f)
+ quadToRelative(48.57f, -16.31f, 89.13f, -48.11f)
+ lineTo(226.8f, 289.98f)
+ quadToRelative(-31.32f, 41.04f, -47.63f, 89.37f)
+ quadToRelative(-16.3f, 48.32f, -16.3f, 100.17f)
+ quadToRelative(0.0f, 132.81f, 92.28f, 225.21f)
+ reflectiveQuadTo(480.0f, 797.13f)
+ close()
+ moveTo(733.43f, 669.07f)
+ quadToRelative(31.09f, -41.05f, 47.4f, -89.37f)
+ quadToRelative(16.3f, -48.33f, 16.3f, -100.18f)
+ quadToRelative(0.0f, -132.56f, -92.28f, -224.61f)
+ quadToRelative(-92.28f, -92.04f, -224.85f, -92.04f)
+ quadToRelative(-51.85f, 0.0f, -100.05f, 16.06f)
+ quadToRelative(-48.21f, 16.07f, -89.25f, 47.16f)
+ lineToRelative(442.73f, 442.98f)
+ close()
+ }
+ }
+ .build()
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Complete.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Complete.kt
new file mode 100644
index 00000000..27d3b328
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Complete.kt
@@ -0,0 +1,78 @@
+package dev.chungjungsoo.gptmobile.presentation.icons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val Complete: ImageVector
+ get() = Builder(
+ name = "Complete Icon",
+ defaultWidth = 24.0.dp,
+ defaultHeight = 24.0.dp,
+ viewportWidth = 960.0f,
+ viewportHeight = 960.0f
+ ).apply {
+ path(
+ fill = SolidColor(Color(0xFF006C50)),
+ stroke = null,
+ strokeLineWidth = 0.0f,
+ strokeLineCap = Butt,
+ strokeLineJoin = Miter,
+ strokeLineMiter = 4.0f,
+ pathFillType = NonZero
+ ) {
+ moveToRelative(423.28f, 543.63f)
+ lineToRelative(-79.78f, -79.78f)
+ quadToRelative(-12.43f, -12.44f, -31.35f, -12.44f)
+ quadToRelative(-18.91f, 0.0f, -31.35f, 12.44f)
+ quadToRelative(-12.43f, 12.43f, -12.31f, 31.35f)
+ quadToRelative(0.12f, 18.91f, 12.55f, 31.34f)
+ lineToRelative(110.18f, 110.18f)
+ quadToRelative(13.76f, 13.67f, 32.11f, 13.67f)
+ quadToRelative(18.34f, 0.0f, 32.02f, -13.67f)
+ lineTo(677.76f, 414.3f)
+ quadToRelative(12.44f, -12.43f, 12.44f, -31.22f)
+ quadToRelative(0.0f, -18.8f, -12.44f, -31.23f)
+ quadToRelative(-12.43f, -12.44f, -31.35f, -12.44f)
+ quadToRelative(-18.91f, 0.0f, -31.34f, 12.44f)
+ lineTo(423.28f, 543.63f)
+ close()
+ moveTo(480.0f, 888.13f)
+ quadToRelative(-84.91f, 0.0f, -159.34f, -32.12f)
+ quadToRelative(-74.44f, -32.12f, -129.5f, -87.17f)
+ quadToRelative(-55.05f, -55.06f, -87.17f, -129.5f)
+ quadTo(71.87f, 564.91f, 71.87f, 480.0f)
+ reflectiveQuadToRelative(32.12f, -159.34f)
+ quadToRelative(32.12f, -74.44f, 87.17f, -129.5f)
+ quadToRelative(55.06f, -55.05f, 129.5f, -87.17f)
+ quadToRelative(74.43f, -32.12f, 159.34f, -32.12f)
+ reflectiveQuadToRelative(159.34f, 32.12f)
+ quadToRelative(74.44f, 32.12f, 129.5f, 87.17f)
+ quadToRelative(55.05f, 55.06f, 87.17f, 129.5f)
+ quadToRelative(32.12f, 74.43f, 32.12f, 159.34f)
+ reflectiveQuadToRelative(-32.12f, 159.34f)
+ quadToRelative(-32.12f, 74.44f, -87.17f, 129.5f)
+ quadToRelative(-55.06f, 55.05f, -129.5f, 87.17f)
+ quadTo(564.91f, 888.13f, 480.0f, 888.13f)
+ close()
+ moveTo(480.0f, 797.13f)
+ quadToRelative(133.04f, 0.0f, 225.09f, -92.04f)
+ quadToRelative(92.04f, -92.05f, 92.04f, -225.09f)
+ quadToRelative(0.0f, -133.04f, -92.04f, -225.09f)
+ quadToRelative(-92.05f, -92.04f, -225.09f, -92.04f)
+ quadToRelative(-133.04f, 0.0f, -225.09f, 92.04f)
+ quadToRelative(-92.04f, 92.05f, -92.04f, 225.09f)
+ quadToRelative(0.0f, 133.04f, 92.04f, 225.09f)
+ quadToRelative(92.05f, 92.04f, 225.09f, 92.04f)
+ close()
+ moveTo(480.0f, 480.0f)
+ close()
+ }
+ }
+ .build()
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Done.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Done.kt
index 3d77752e..feeb0350 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Done.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Done.kt
@@ -16,7 +16,7 @@ val Done: ImageVector
@Composable
get() {
return Builder(
- name = "IcDone",
+ name = "Done Icon",
defaultWidth = 48.0.dp,
defaultHeight = 48.0.dp,
viewportWidth = 960.0f,
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Error.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Error.kt
new file mode 100644
index 00000000..20f4ec5d
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Error.kt
@@ -0,0 +1,86 @@
+package dev.chungjungsoo.gptmobile.presentation.icons
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val Error: ImageVector
+ @Composable
+ get() = Builder(
+ name = "Error Icon",
+ defaultWidth = 24.0.dp,
+ defaultHeight = 24.0.dp,
+ viewportWidth = 960.0f,
+ viewportHeight = 960.0f
+ ).apply {
+ path(
+ fill = SolidColor(MaterialTheme.colorScheme.error),
+ stroke = null,
+ strokeLineWidth = 0.0f,
+ strokeLineCap = Butt,
+ strokeLineJoin = Miter,
+ strokeLineMiter = 4.0f,
+ pathFillType = NonZero
+ ) {
+ moveTo(479.95f, 688.13f)
+ quadToRelative(19.92f, 0.0f, 33.45f, -13.48f)
+ quadToRelative(13.53f, -13.48f, 13.53f, -33.4f)
+ quadToRelative(0.0f, -19.92f, -13.47f, -33.58f)
+ quadToRelative(-13.48f, -13.65f, -33.41f, -13.65f)
+ quadToRelative(-19.92f, 0.0f, -33.45f, 13.65f)
+ quadToRelative(-13.53f, 13.66f, -13.53f, 33.58f)
+ quadToRelative(0.0f, 19.92f, 13.47f, 33.4f)
+ quadToRelative(13.48f, 13.48f, 33.41f, 13.48f)
+ close()
+ moveTo(480.0f, 520.48f)
+ quadToRelative(19.15f, 0.0f, 32.33f, -13.18f)
+ quadToRelative(13.17f, -13.17f, 13.17f, -32.32f)
+ verticalLineToRelative(-154.5f)
+ quadToRelative(0.0f, -19.15f, -13.17f, -32.33f)
+ quadToRelative(-13.18f, -13.17f, -32.33f, -13.17f)
+ reflectiveQuadToRelative(-32.33f, 13.17f)
+ quadToRelative(-13.17f, 13.18f, -13.17f, 32.33f)
+ verticalLineToRelative(154.5f)
+ quadToRelative(0.0f, 19.15f, 13.17f, 32.32f)
+ quadToRelative(13.18f, 13.18f, 32.33f, 13.18f)
+ close()
+ moveTo(480.0f, 888.13f)
+ quadToRelative(-84.91f, 0.0f, -159.34f, -32.12f)
+ quadToRelative(-74.44f, -32.12f, -129.5f, -87.17f)
+ quadToRelative(-55.05f, -55.06f, -87.17f, -129.5f)
+ quadTo(71.87f, 564.91f, 71.87f, 480.0f)
+ reflectiveQuadToRelative(32.12f, -159.34f)
+ quadToRelative(32.12f, -74.44f, 87.17f, -129.5f)
+ quadToRelative(55.06f, -55.05f, 129.5f, -87.17f)
+ quadToRelative(74.43f, -32.12f, 159.34f, -32.12f)
+ reflectiveQuadToRelative(159.34f, 32.12f)
+ quadToRelative(74.44f, 32.12f, 129.5f, 87.17f)
+ quadToRelative(55.05f, 55.06f, 87.17f, 129.5f)
+ quadToRelative(32.12f, 74.43f, 32.12f, 159.34f)
+ reflectiveQuadToRelative(-32.12f, 159.34f)
+ quadToRelative(-32.12f, 74.44f, -87.17f, 129.5f)
+ quadToRelative(-55.06f, 55.05f, -129.5f, 87.17f)
+ quadTo(564.91f, 888.13f, 480.0f, 888.13f)
+ close()
+ moveTo(480.0f, 797.13f)
+ quadToRelative(133.04f, 0.0f, 225.09f, -92.04f)
+ quadToRelative(92.04f, -92.05f, 92.04f, -225.09f)
+ quadToRelative(0.0f, -133.04f, -92.04f, -225.09f)
+ quadToRelative(-92.05f, -92.04f, -225.09f, -92.04f)
+ quadToRelative(-133.04f, 0.0f, -225.09f, 92.04f)
+ quadToRelative(-92.04f, 92.05f, -92.04f, 225.09f)
+ quadToRelative(0.0f, 133.04f, 92.04f, 225.09f)
+ quadToRelative(92.05f, 92.04f, 225.09f, 92.04f)
+ close()
+ moveTo(480.0f, 480.0f)
+ close()
+ }
+ }
+ .build()
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Migrating.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Migrating.kt
new file mode 100644
index 00000000..394698c0
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Migrating.kt
@@ -0,0 +1,93 @@
+package dev.chungjungsoo.gptmobile.presentation.icons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val Migrating: ImageVector
+ get() = Builder(
+ name = "Migrating Icon",
+ defaultWidth = 24.0.dp,
+ defaultHeight =
+ 24.0.dp,
+ viewportWidth = 960.0f,
+ viewportHeight = 960.0f
+ ).apply {
+ path(
+ fill = SolidColor(Color(0xFF006C50)),
+ stroke = null,
+ strokeLineWidth = 0.0f,
+ strokeLineCap = Butt,
+ strokeLineJoin = Miter,
+ strokeLineMiter = 4.0f,
+ pathFillType = NonZero
+ ) {
+ moveTo(245.02f, 482.0f)
+ quadToRelative(0.0f, 43.57f, 16.64f, 84.99f)
+ quadToRelative(16.64f, 41.42f, 51.69f, 76.94f)
+ lineToRelative(8.08f, 8.09f)
+ verticalLineToRelative(-50.35f)
+ quadToRelative(0.0f, -17.71f, 12.1f, -29.69f)
+ reflectiveQuadTo(363.35f, 560.0f)
+ quadToRelative(17.72f, 0.0f, 29.69f, 11.98f)
+ quadToRelative(11.98f, 11.98f, 11.98f, 29.69f)
+ verticalLineToRelative(157.61f)
+ quadToRelative(0.0f, 19.15f, -13.17f, 32.33f)
+ quadToRelative(-13.18f, 13.17f, -32.33f, 13.17f)
+ lineTo(202.15f, 804.78f)
+ quadToRelative(-17.72f, 0.0f, -29.69f, -12.1f)
+ quadToRelative(-11.98f, -12.09f, -11.98f, -29.81f)
+ reflectiveQuadToRelative(12.1f, -29.7f)
+ quadToRelative(12.09f, -11.97f, 29.81f, -11.97f)
+ horizontalLineToRelative(60.2f)
+ lineToRelative(-12.89f, -11.61f)
+ quadToRelative(-52.24f, -47.44f, -73.96f, -107.16f)
+ quadToRelative(-21.72f, -59.71f, -21.72f, -120.43f)
+ quadToRelative(0.0f, -96.39f, 50.03f, -174.92f)
+ quadToRelative(50.04f, -78.54f, 133.91f, -119.78f)
+ quadToRelative(15.67f, -8.47f, 32.25f, 0.08f)
+ quadToRelative(16.57f, 8.55f, 22.05f, 26.71f)
+ quadToRelative(5.24f, 17.15f, -1.34f, 33.94f)
+ quadToRelative(-6.57f, 16.8f, -22.49f, 25.75f)
+ quadToRelative(-56.32f, 31.29f, -89.86f, 86.59f)
+ quadToRelative(-33.55f, 55.3f, -33.55f, 121.63f)
+ close()
+ moveTo(714.98f, 478.0f)
+ quadToRelative(0.0f, -43.57f, -16.64f, -84.99f)
+ quadToRelative(-16.64f, -41.42f, -51.69f, -76.94f)
+ lineToRelative(-8.08f, -8.09f)
+ verticalLineToRelative(50.35f)
+ quadToRelative(0.0f, 17.71f, -11.98f, 29.69f)
+ reflectiveQuadTo(596.89f, 400.0f)
+ quadToRelative(-17.72f, 0.0f, -29.81f, -12.1f)
+ quadToRelative(-12.1f, -12.1f, -12.1f, -29.81f)
+ verticalLineToRelative(-157.37f)
+ quadToRelative(0.0f, -19.15f, 13.17f, -32.33f)
+ quadToRelative(13.18f, -13.17f, 32.33f, -13.17f)
+ horizontalLineToRelative(157.37f)
+ quadToRelative(17.72f, 0.0f, 29.69f, 11.98f)
+ quadToRelative(11.98f, 11.97f, 11.98f, 29.69f)
+ reflectiveQuadToRelative(-11.98f, 29.82f)
+ quadToRelative(-11.97f, 12.09f, -29.69f, 12.09f)
+ horizontalLineToRelative(-60.44f)
+ lineToRelative(12.89f, 11.61f)
+ quadToRelative(49.72f, 49.96f, 72.7f, 108.42f)
+ quadToRelative(22.98f, 58.45f, 22.98f, 119.17f)
+ quadToRelative(0.0f, 96.39f, -50.03f, 174.92f)
+ quadToRelative(-50.04f, 78.54f, -133.91f, 119.78f)
+ quadToRelative(-15.67f, 8.47f, -32.25f, -0.08f)
+ quadToRelative(-16.57f, -8.55f, -22.05f, -26.71f)
+ quadToRelative(-5.24f, -17.15f, 1.34f, -33.94f)
+ quadToRelative(6.57f, -16.8f, 22.49f, -25.75f)
+ quadToRelative(56.32f, -31.29f, 89.86f, -86.59f)
+ quadToRelative(33.55f, -55.3f, 33.55f, -121.63f)
+ close()
+ }
+ }
+ .build()
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Ready.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Ready.kt
new file mode 100644
index 00000000..287d270c
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/icons/Ready.kt
@@ -0,0 +1,166 @@
+package dev.chungjungsoo.gptmobile.presentation.icons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val Ready: ImageVector
+ get() = Builder(
+ name = "Ready Icon",
+ defaultWidth = 24.0.dp,
+ defaultHeight = 24.0.dp,
+ viewportWidth = 960.0f,
+ viewportHeight = 960.0f
+ ).apply {
+ path(
+ fill = SolidColor(Color(0xFF006C50)),
+ stroke = null,
+ strokeLineWidth = 0.0f,
+ strokeLineCap = Butt,
+ strokeLineJoin = Miter,
+ strokeLineMiter = 4.0f,
+ pathFillType = NonZero
+ ) {
+ moveTo(174.54f, 564.78f)
+ quadToRelative(5.29f, 19.89f, 12.59f, 38.02f)
+ quadToRelative(7.3f, 18.13f, 16.63f, 34.5f)
+ quadToRelative(9.72f, 17.63f, 7.36f, 36.91f)
+ quadToRelative(-2.36f, 19.27f, -15.03f, 31.94f)
+ quadToRelative(-12.92f, 12.92f, -31.33f, 12.06f)
+ quadToRelative(-18.41f, -0.86f, -28.37f, -16.3f)
+ quadToRelative(-20.28f, -31.28f, -34.42f, -64.56f)
+ reflectiveQuadToRelative(-22.14f, -70.57f)
+ quadToRelative(-4.24f, -17.91f, 7.69f, -32.35f)
+ quadTo(99.46f, 520.0f, 118.61f, 520.0f)
+ reflectiveQuadToRelative(34.56f, 12.32f)
+ quadToRelative(15.42f, 12.31f, 21.37f, 32.46f)
+ close()
+ moveTo(203.76f, 322.7f)
+ quadToRelative(-9.33f, 16.37f, -16.51f, 34.62f)
+ quadToRelative(-7.18f, 18.25f, -12.47f, 37.9f)
+ quadToRelative(-5.95f, 20.15f, -21.49f, 32.46f)
+ quadTo(137.76f, 440.0f, 118.61f, 440.0f)
+ reflectiveQuadToRelative(-31.09f, -13.79f)
+ quadToRelative(-11.93f, -13.8f, -7.69f, -31.71f)
+ quadToRelative(8.0f, -38.04f, 22.4f, -72.73f)
+ quadToRelative(14.4f, -34.68f, 34.44f, -64.2f)
+ quadToRelative(9.96f, -14.92f, 28.23f, -15.78f)
+ quadToRelative(18.27f, -0.86f, 31.19f, 12.06f)
+ quadToRelative(12.67f, 12.67f, 15.03f, 31.94f)
+ quadToRelative(2.36f, 19.28f, -7.36f, 36.91f)
+ close()
+ moveTo(321.7f, 754.24f)
+ quadToRelative(17.37f, 10.33f, 35.62f, 18.01f)
+ quadToRelative(18.25f, 7.68f, 37.14f, 12.97f)
+ quadToRelative(19.39f, 6.19f, 31.58f, 21.23f)
+ quadToRelative(12.2f, 15.03f, 12.2f, 34.18f)
+ reflectiveQuadToRelative(-13.67f, 30.71f)
+ quadToRelative(-13.68f, 11.55f, -31.83f, 8.07f)
+ quadToRelative(-36.04f, -8.0f, -69.83f, -21.52f)
+ quadToRelative(-33.78f, -13.52f, -65.06f, -33.8f)
+ quadToRelative(-15.68f, -9.96f, -17.3f, -28.73f)
+ quadToRelative(-1.62f, -18.77f, 11.3f, -32.45f)
+ quadToRelative(13.43f, -13.67f, 32.82f, -16.03f)
+ quadToRelative(19.4f, -2.36f, 37.03f, 7.36f)
+ close()
+ moveTo(396.46f, 174.07f)
+ quadToRelative(-19.13f, 5.28f, -36.88f, 12.7f)
+ quadToRelative(-17.75f, 7.43f, -35.12f, 17.75f)
+ quadToRelative(-18.39f, 9.96f, -38.05f, 7.98f)
+ quadToRelative(-19.65f, -1.98f, -33.32f, -15.65f)
+ quadToRelative(-13.68f, -13.92f, -12.56f, -32.07f)
+ quadToRelative(1.12f, -18.15f, 17.04f, -28.35f)
+ quadToRelative(32.04f, -20.28f, 66.82f, -34.42f)
+ quadToRelative(34.78f, -14.14f, 71.83f, -21.9f)
+ quadToRelative(16.91f, -3.48f, 30.46f, 8.19f)
+ quadToRelative(13.56f, 11.68f, 13.56f, 30.83f)
+ reflectiveQuadToRelative(-12.32f, 34.07f)
+ quadToRelative(-12.31f, 14.91f, -31.46f, 20.87f)
+ close()
+ moveTo(636.3f, 755.24f)
+ quadToRelative(17.63f, -9.96f, 37.29f, -7.98f)
+ quadToRelative(19.65f, 1.98f, 33.32f, 15.65f)
+ quadToRelative(13.68f, 13.68f, 12.56f, 32.45f)
+ quadToRelative(-1.12f, 18.77f, -16.8f, 27.97f)
+ quadToRelative(-31.28f, 20.28f, -66.32f, 34.3f)
+ quadToRelative(-35.05f, 14.02f, -72.09f, 22.02f)
+ quadToRelative(-17.91f, 3.48f, -31.97f, -8.19f)
+ quadToRelative(-14.05f, -11.68f, -14.05f, -30.83f)
+ reflectiveQuadToRelative(12.69f, -33.95f)
+ quadToRelative(12.7f, -14.79f, 32.09f, -20.98f)
+ quadToRelative(19.65f, -5.29f, 37.78f, -12.71f)
+ quadToRelative(18.13f, -7.42f, 35.5f, -17.75f)
+ close()
+ moveTo(564.02f, 174.3f)
+ quadToRelative(-19.39f, -5.95f, -31.59f, -20.98f)
+ quadToRelative(-12.19f, -15.04f, -12.19f, -34.19f)
+ reflectiveQuadToRelative(13.67f, -30.71f)
+ quadToRelative(13.68f, -11.55f, 30.83f, -8.07f)
+ quadToRelative(36.8f, 8.0f, 71.73f, 21.9f)
+ quadToRelative(34.92f, 13.9f, 66.2f, 34.18f)
+ quadToRelative(15.68f, 10.2f, 16.92f, 28.35f)
+ quadToRelative(1.24f, 18.15f, -11.68f, 31.83f)
+ quadToRelative(-13.43f, 13.67f, -32.7f, 16.03f)
+ quadToRelative(-19.28f, 2.36f, -37.67f, -7.36f)
+ quadToRelative(-18.13f, -10.32f, -36.38f, -18.01f)
+ quadToRelative(-18.25f, -7.68f, -37.14f, -12.97f)
+ close()
+ moveTo(785.46f, 563.54f)
+ quadToRelative(5.71f, -19.39f, 21.25f, -31.7f)
+ quadToRelative(15.53f, -12.32f, 34.68f, -12.32f)
+ reflectiveQuadToRelative(31.09f, 14.44f)
+ quadToRelative(11.93f, 14.43f, 7.69f, 32.34f)
+ quadToRelative(-8.0f, 37.29f, -22.9f, 71.57f)
+ quadToRelative(-14.9f, 34.28f, -34.18f, 63.8f)
+ quadToRelative(-9.96f, 14.68f, -28.11f, 15.92f)
+ quadToRelative(-18.15f, 1.24f, -31.07f, -11.68f)
+ quadToRelative(-12.67f, -12.67f, -15.03f, -32.44f)
+ quadToRelative(-2.36f, -19.77f, 7.36f, -37.4f)
+ quadToRelative(9.33f, -17.14f, 16.63f, -35.0f)
+ quadToRelative(7.3f, -17.87f, 12.59f, -37.53f)
+ close()
+ moveTo(756.0f, 321.98f)
+ quadToRelative(-9.48f, -17.39f, -7.12f, -36.55f)
+ quadToRelative(2.36f, -19.15f, 15.03f, -31.82f)
+ quadToRelative(12.92f, -12.91f, 30.95f, -11.68f)
+ quadToRelative(18.03f, 1.24f, 28.23f, 16.16f)
+ quadToRelative(21.04f, 31.04f, 35.44f, 64.96f)
+ quadToRelative(14.4f, 33.93f, 22.4f, 70.97f)
+ quadToRelative(3.48f, 17.91f, -8.45f, 31.71f)
+ quadToRelative(-11.94f, 13.79f, -31.09f, 13.79f)
+ reflectiveQuadToRelative(-34.68f, -12.31f)
+ quadToRelative(-15.54f, -12.32f, -21.49f, -32.71f)
+ quadToRelative(-5.29f, -19.89f, -12.59f, -38.02f)
+ quadToRelative(-7.3f, -18.13f, -16.63f, -34.5f)
+ close()
+ moveTo(479.24f, 682.39f)
+ quadToRelative(-17.72f, 0.0f, -29.82f, -11.98f)
+ quadToRelative(-12.09f, -11.98f, -12.09f, -29.69f)
+ lineTo(437.33f, 439.3f)
+ lineToRelative(-71.9f, 72.66f)
+ quadToRelative(-12.47f, 12.47f, -29.69f, 12.47f)
+ reflectiveQuadToRelative(-29.7f, -12.47f)
+ quadToRelative(-12.47f, -12.48f, -12.86f, -29.82f)
+ quadToRelative(-0.38f, -17.34f, 12.1f, -29.81f)
+ lineToRelative(142.13f, -142.66f)
+ quadTo(460.33f, 297.0f, 479.24f, 297.0f)
+ quadToRelative(18.91f, 0.0f, 31.83f, 12.67f)
+ lineTo(651.43f, 449.8f)
+ quadToRelative(12.48f, 12.48f, 12.86f, 30.2f)
+ quadToRelative(0.38f, 17.72f, -12.09f, 30.2f)
+ quadToRelative(-12.48f, 12.47f, -30.08f, 12.47f)
+ quadToRelative(-17.6f, 0.0f, -30.08f, -12.47f)
+ lineToRelative(-71.13f, -70.9f)
+ verticalLineToRelative(201.42f)
+ quadToRelative(0.0f, 17.71f, -11.98f, 29.69f)
+ quadToRelative(-11.97f, 11.98f, -29.69f, 11.98f)
+ close()
+ }
+ }
+ .build()
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatBubble.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatBubble.kt
index e11ef0f7..ed4e2a23 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatBubble.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatBubble.kt
@@ -1,47 +1,54 @@
package dev.chungjungsoo.gptmobile.presentation.ui.chat
-import android.text.util.Linkify
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Edit
-import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Refresh
-import androidx.compose.material3.AssistChip
-import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
+import com.halilibo.richtext.markdown.BasicMarkdown
+import com.halilibo.richtext.ui.material3.RichText
import dev.chungjungsoo.gptmobile.R
-import dev.chungjungsoo.gptmobile.data.model.ApiType
import dev.chungjungsoo.gptmobile.presentation.theme.GPTMobileTheme
-import dev.chungjungsoo.gptmobile.util.getPlatformAPIBrandText
-import dev.jeziellago.compose.markdowntext.MarkdownText
@Composable
fun UserChatBubble(
modifier: Modifier = Modifier,
text: String,
- isLoading: Boolean,
- onEditClick: () -> Unit,
- onCopyClick: () -> Unit
+ onLongPress: () -> Unit
) {
val cardColor = CardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
@@ -49,26 +56,21 @@ fun UserChatBubble(
disabledContentColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.38f),
disabledContainerColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.38f)
)
+ val parser = remember { CommonmarkAstNodeParser() }
+ val astNode = remember(text) { parser.parse(text.trimIndent()) }
Column(horizontalAlignment = Alignment.End) {
Card(
- modifier = modifier,
+ modifier = modifier
+ .pointerInput(Unit) {
+ detectTapGestures(onLongPress = { onLongPress.invoke() })
+ },
shape = RoundedCornerShape(32.dp),
colors = cardColor
) {
- MarkdownText(
- modifier = Modifier.padding(16.dp),
- markdown = text,
- isTextSelectable = true,
- linkifyMask = Linkify.WEB_URLS
- )
- }
- Row {
- if (!isLoading) {
- EditTextChip(onEditClick)
- Spacer(modifier = Modifier.width(8.dp))
+ RichText(modifier = Modifier.padding(16.dp)) {
+ BasicMarkdown(astNode = astNode)
}
- CopyTextChip(onCopyClick)
}
}
}
@@ -79,43 +81,43 @@ fun OpponentChatBubble(
canRetry: Boolean,
isLoading: Boolean,
isError: Boolean = false,
- apiType: ApiType,
text: String,
onCopyClick: () -> Unit = {},
+ onSelectClick: () -> Unit = {},
onRetryClick: () -> Unit = {}
) {
val cardColor = CardColors(
- containerColor = MaterialTheme.colorScheme.secondaryContainer,
- contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
- disabledContentColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.38f),
- disabledContainerColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.38f)
+ containerColor = MaterialTheme.colorScheme.background,
+ contentColor = MaterialTheme.colorScheme.onBackground,
+ disabledContentColor = MaterialTheme.colorScheme.background.copy(alpha = 0.38f),
+ disabledContainerColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.38f)
)
+ val parser = remember { CommonmarkAstNodeParser() }
+ val astNode = remember(text) { parser.parse(text.trimIndent() + if (isLoading) "●" else "") }
Column(modifier = modifier) {
- Column(horizontalAlignment = Alignment.End) {
+ Column {
Card(
- shape = RoundedCornerShape(32.dp),
+ shape = RoundedCornerShape(0.dp),
colors = cardColor
) {
- MarkdownText(
- modifier = Modifier.padding(24.dp),
- markdown = text.trimIndent() + if (isLoading) "▊" else "",
- isTextSelectable = true,
- linkifyMask = Linkify.WEB_URLS
- )
- if (!isLoading) {
- BrandText(apiType)
+ RichText(modifier = Modifier.padding(16.dp)) {
+ BasicMarkdown(astNode = astNode)
}
}
if (!isLoading) {
- Row {
+ Row(
+ modifier = Modifier.padding(start = 16.dp)
+ ) {
if (!isError) {
- CopyTextChip(onCopyClick)
+ CopyTextIcon(onCopyClick)
+ Spacer(modifier = Modifier.width(8.dp))
+ SelectTextIcon(onSelectClick)
}
if (canRetry) {
Spacer(modifier = Modifier.width(8.dp))
- RetryChip(onRetryClick)
+ RetryIcon(onRetryClick)
}
}
}
@@ -124,62 +126,87 @@ fun OpponentChatBubble(
}
@Composable
-private fun EditTextChip(onEditClick: () -> Unit) {
- AssistChip(
- onClick = onEditClick,
- label = { Text(stringResource(R.string.edit)) },
- leadingIcon = {
- Icon(
- Icons.Outlined.Edit,
- contentDescription = stringResource(R.string.edit),
- modifier = Modifier.size(AssistChipDefaults.IconSize)
+fun GPTMobileIcon(loading: Boolean) {
+ Box(
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(40.dp)
+ .clip(RoundedCornerShape(40.dp))
+ .background(color = Color(0xFF00A67D)),
+ contentAlignment = Alignment.Center
+ ) {
+ if (loading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(40.dp)
)
}
- )
+ Image(
+ painter = painterResource(R.drawable.ic_gpt_mobile_no_padding),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp)
+ )
+ }
}
@Composable
-private fun CopyTextChip(onCopyClick: () -> Unit) {
- AssistChip(
- onClick = onCopyClick,
- label = { Text(stringResource(R.string.copy_text)) },
- leadingIcon = {
- Icon(
- imageVector = ImageVector.vectorResource(id = R.drawable.ic_copy),
- contentDescription = stringResource(R.string.copy_text),
- modifier = Modifier.size(AssistChipDefaults.IconSize)
- )
+fun PlatformButton(
+ isLoading: Boolean,
+ name: String,
+ selected: Boolean,
+ onPlatformClick: () -> Unit
+) {
+ val buttonContent: @Composable RowScope.() -> Unit = {
+ Spacer(modifier = Modifier.width(12.dp))
+
+ if (isLoading) {
+ CircularProgressIndicator(modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(8.dp))
}
+
+ Text(
+ text = name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = if (selected) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ if (isLoading) Spacer(modifier = Modifier.width(4.dp))
+ }
+
+ TextButton(
+ modifier = Modifier.widthIn(max = 160.dp),
+ onClick = onPlatformClick,
+ colors = if (selected) ButtonDefaults.filledTonalButtonColors() else ButtonDefaults.textButtonColors(),
+ content = buttonContent
)
}
@Composable
-private fun RetryChip(onRetryClick: () -> Unit) {
- AssistChip(
- onClick = onRetryClick,
- label = { Text(stringResource(R.string.retry)) },
- leadingIcon = {
- Icon(
- Icons.Rounded.Refresh,
- contentDescription = stringResource(R.string.retry),
- modifier = Modifier.size(AssistChipDefaults.IconSize)
- )
- }
- )
+private fun CopyTextIcon(onCopyClick: () -> Unit) {
+ IconButton(onClick = onCopyClick) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_copy),
+ contentDescription = stringResource(R.string.copy_text)
+ )
+ }
}
@Composable
-private fun BrandText(apiType: ApiType) {
- Box(
- modifier = Modifier
- .padding(start = 24.dp, end = 24.dp, bottom = 16.dp)
- .fillMaxWidth()
- ) {
- Text(
- modifier = Modifier.align(Alignment.CenterEnd),
- text = getPlatformAPIBrandText(apiType),
- style = MaterialTheme.typography.labelLarge,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+private fun SelectTextIcon(onSelectClick: () -> Unit) {
+ IconButton(onClick = onSelectClick) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_select),
+ contentDescription = stringResource(R.string.select_text)
+ )
+ }
+}
+
+@Composable
+private fun RetryIcon(onRetryClick: () -> Unit) {
+ IconButton(onClick = onRetryClick) {
+ Icon(
+ Icons.Rounded.Refresh,
+ contentDescription = stringResource(R.string.retry)
)
}
}
@@ -192,7 +219,7 @@ fun UserChatBubblePreview() {
in Python?
""".trimIndent()
GPTMobileTheme {
- UserChatBubble(text = sampleText, isLoading = false, onCopyClick = {}, onEditClick = {})
+ UserChatBubble(text = sampleText, onLongPress = {})
}
}
@@ -219,7 +246,6 @@ fun OpponentChatBubblePreview() {
text = sampleText,
canRetry = true,
isLoading = false,
- apiType = ApiType.OPENAI,
onCopyClick = {},
onRetryClick = {}
)
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatDialogs.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatDialogs.kt
index caf72922..06cedc5c 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatDialogs.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatDialogs.kt
@@ -1,25 +1,13 @@
package dev.chungjungsoo.gptmobile.presentation.ui.chat
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Done
-import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.FilledTonalButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -32,21 +20,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import dev.chungjungsoo.gptmobile.R
-import dev.chungjungsoo.gptmobile.data.database.entity.Message
+import dev.chungjungsoo.gptmobile.data.database.entity.MessageV2
@Composable
fun ChatTitleDialog(
initialTitle: String,
- aiCoreModeEnabled: Boolean,
- aiGeneratedResult: String,
- isAICoreLoading: Boolean,
onDefaultTitleMode: () -> String?,
- onAICoreTitleMode: () -> Unit,
- onRetryRequest: () -> Unit,
onConfirmRequest: (title: String) -> Unit,
onDismissRequest: () -> Unit
) {
@@ -63,7 +45,6 @@ fun ChatTitleDialog(
title = { Text(text = stringResource(R.string.chat_title)) },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
- Text(text = stringResource(R.string.chat_title_dialog_description))
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
@@ -79,67 +60,6 @@ fun ChatTitleDialog(
onValueChange = { title = it },
label = { Text(stringResource(R.string.chat_title)) }
)
- Row(
- modifier = Modifier.fillMaxWidth()
- ) {
- FilledTonalButton(
- modifier = Modifier
- .padding(horizontal = 8.dp)
- .height(48.dp)
- .weight(1F),
- enabled = !isAICoreLoading,
- onClick = { title = onDefaultTitleMode.invoke() ?: untitledChat }
- ) { Text(text = stringResource(R.string.default_mode)) }
-
- FilledTonalButton(
- enabled = aiCoreModeEnabled && !isAICoreLoading,
- modifier = Modifier
- .padding(horizontal = 8.dp)
- .height(48.dp)
- .weight(1F),
- onClick = {
- onAICoreTitleMode.invoke()
- useAICore = true
- }
- ) { Text(text = stringResource(R.string.ai_generated)) }
- }
-
- if (useAICore) {
- Card(
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.surfaceVariant
- ),
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 16.dp)
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(min = 64.dp)
- .padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 8.dp)
- ) {
- Text(
- text = aiGeneratedResult.trimIndent() + if (isAICoreLoading) "▊" else "",
- fontWeight = FontWeight.Bold
- )
- Row(modifier = Modifier.fillMaxWidth()) {
- Spacer(Modifier.weight(1f))
- if (!isAICoreLoading) {
- IconButton(
- onClick = {
- title = aiGeneratedResult.trimIndent().replace('\n', ' ')
- useAICore = false
- }
- ) { Icon(Icons.Default.Done, contentDescription = stringResource(R.string.apply_generated_title)) }
- IconButton(
- onClick = onRetryRequest
- ) { Icon(Icons.Rounded.Refresh, contentDescription = stringResource(R.string.retry_ai_title)) }
- }
- }
- }
- }
- }
}
},
onDismissRequest = onDismissRequest,
@@ -155,6 +75,11 @@ fun ChatTitleDialog(
}
},
dismissButton = {
+ TextButton(
+ onClick = { title = onDefaultTitleMode.invoke() ?: untitledChat }
+ ) {
+ Text(text = stringResource(R.string.default_mode))
+ }
TextButton(
onClick = onDismissRequest
) {
@@ -166,9 +91,9 @@ fun ChatTitleDialog(
@Composable
fun ChatQuestionEditDialog(
- initialQuestion: Message,
+ initialQuestion: MessageV2,
onDismissRequest: () -> Unit,
- onConfirmRequest: (q: Message) -> Unit
+ onConfirmRequest: (MessageV2) -> Unit
) {
val configuration = LocalConfiguration.current
var question by remember { mutableStateOf(initialQuestion.content) }
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt
index 95634277..1af499a1 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatScreen.kt
@@ -2,16 +2,15 @@ package dev.chungjungsoo.gptmobile.presentation.ui.chat
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast
-import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -26,11 +25,15 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.DropdownMenu
@@ -42,6 +45,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text
@@ -54,7 +58,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -77,12 +80,7 @@ import androidx.core.content.FileProvider.getUriForFile
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chungjungsoo.gptmobile.R
-import dev.chungjungsoo.gptmobile.data.database.entity.Message
-import dev.chungjungsoo.gptmobile.data.model.ApiType
-import dev.chungjungsoo.gptmobile.util.DefaultHashMap
-import dev.chungjungsoo.gptmobile.util.multiScrollStateSaver
import java.io.File
-import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -94,65 +92,35 @@ fun ChatScreen(
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val focusManager = LocalFocusManager.current
val clipboardManager = LocalClipboardManager.current
- val packageManager = LocalContext.current.packageManager
val systemChatMargin = 32.dp
- val maximumChatBubbleWidth = screenWidth - 48.dp - systemChatMargin
+ val maximumUserChatBubbleWidth = (screenWidth - systemChatMargin) * 0.8F
+ val maximumOpponentChatBubbleWidth = screenWidth - systemChatMargin
val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
- val aiCorePackageInfo = try {
- packageManager.getPackageInfo("com.google.android.aicore", 0)
- } catch (_: PackageManager.NameNotFoundException) {
- null
- }
- val privateComputePackageInfo = try {
- packageManager.getPackageInfo("com.google.android.as.oss", 0)
- } catch (_: PackageManager.NameNotFoundException) {
- null
- }
-
val chatRoom by chatViewModel.chatRoom.collectAsStateWithLifecycle()
+ val groupedMessages by chatViewModel.groupedMessages.collectAsStateWithLifecycle()
+ val chatStates by chatViewModel.chatStates.collectAsStateWithLifecycle()
val isChatTitleDialogOpen by chatViewModel.isChatTitleDialogOpen.collectAsStateWithLifecycle()
val isEditQuestionDialogOpen by chatViewModel.isEditQuestionDialogOpen.collectAsStateWithLifecycle()
+ val isSelectTextSheetOpen by chatViewModel.isSelectTextSheetOpen.collectAsStateWithLifecycle()
val isIdle by chatViewModel.isIdle.collectAsStateWithLifecycle()
val isLoaded by chatViewModel.isLoaded.collectAsStateWithLifecycle()
- val messages by chatViewModel.messages.collectAsStateWithLifecycle()
val question by chatViewModel.question.collectAsStateWithLifecycle()
val appEnabledPlatforms by chatViewModel.enabledPlatformsInApp.collectAsStateWithLifecycle()
- val editedQuestion by chatViewModel.editedQuestion.collectAsStateWithLifecycle()
- val openaiLoadingState by chatViewModel.openaiLoadingState.collectAsStateWithLifecycle()
- val anthropicLoadingState by chatViewModel.anthropicLoadingState.collectAsStateWithLifecycle()
- val googleLoadingState by chatViewModel.googleLoadingState.collectAsStateWithLifecycle()
- val groqLoadingState by chatViewModel.groqLoadingState.collectAsStateWithLifecycle()
- val ollamaLoadingState by chatViewModel.ollamaLoadingState.collectAsStateWithLifecycle()
- val geminiNanoLoadingState by chatViewModel.geminiNanoLoadingState.collectAsStateWithLifecycle()
- val userMessage by chatViewModel.userMessage.collectAsStateWithLifecycle()
- val openAIMessage by chatViewModel.openAIMessage.collectAsStateWithLifecycle()
- val anthropicMessage by chatViewModel.anthropicMessage.collectAsStateWithLifecycle()
- val googleMessage by chatViewModel.googleMessage.collectAsStateWithLifecycle()
- val groqMessage by chatViewModel.groqMessage.collectAsStateWithLifecycle()
- val ollamaMessage by chatViewModel.ollamaMessage.collectAsStateWithLifecycle()
- val geminiNano by chatViewModel.geminiNanoMessage.collectAsStateWithLifecycle()
- val canUseChat = (chatViewModel.enabledPlatformsInChat.toSet() - appEnabledPlatforms.toSet()).isEmpty()
- val groupedMessages = remember(messages) { groupMessages(messages) }
- val latestMessageIndex = groupedMessages.keys.maxOrNull() ?: 0
- val chatBubbleScrollStates = rememberSaveable(saver = multiScrollStateSaver) { DefaultHashMap { ScrollState(0) } }
- val canEnableAICoreMode = rememberSaveable { checkAICoreAvailability(aiCorePackageInfo, privateComputePackageInfo) }
+ val canUseChat = (chatViewModel.enabledPlatformsInChat.toSet() - appEnabledPlatforms.map { it.uid }.toSet()).isEmpty()
val context = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(isIdle) {
- listState.animateScrollToItem(groupedMessages.keys.size)
+ listState.animateScrollToItem(groupedMessages.userMessages.size * 2)
}
LaunchedEffect(isLoaded) {
- delay(300)
- listState.animateScrollToItem(groupedMessages.keys.size)
+ listState.animateScrollToItem(groupedMessages.userMessages.size * 2)
}
- Log.d("AIPackage", "AICore: ${aiCorePackageInfo?.versionName ?: "Not installed"}, Private Compute Services: ${privateComputePackageInfo?.versionName ?: "Not installed"}")
-
Scaffold(
modifier = Modifier
.fillMaxSize()
@@ -186,122 +154,93 @@ fun ChatScreen(
if (listState.canScrollForward) {
ScrollToBottomButton {
scope.launch {
- listState.animateScrollToItem(groupedMessages.keys.size)
+ listState.animateScrollToItem(groupedMessages.userMessages.size * 2)
}
}
}
},
floatingActionButtonPosition = FabPosition.Center
) { innerPadding ->
- groupedMessages.forEach { (i, k) -> Log.d("grouped", "idx: $i, data: $k") }
LazyColumn(
modifier = Modifier.padding(innerPadding),
state = listState
) {
- groupedMessages.keys.sorted().forEach { key ->
- if (key % 2 == 0) {
- // User
- item {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp)
- ) {
- Spacer(modifier = Modifier.weight(1f))
- UserChatBubble(
- modifier = Modifier.widthIn(max = maximumChatBubbleWidth),
- text = groupedMessages[key]!![0].content,
- isLoading = !isIdle,
- onCopyClick = { clipboardManager.setText(AnnotatedString(groupedMessages[key]!![0].content.trim())) },
- onEditClick = { chatViewModel.openEditQuestionDialog(groupedMessages[key]!![0]) }
- )
- }
- }
- } else {
- // Assistant
- item {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .horizontalScroll(chatBubbleScrollStates[(key - 1) / 2])
- ) {
- Spacer(modifier = Modifier.width(8.dp))
- groupedMessages[key]!!.sortedBy { it.platformType }.forEach { m ->
- m.platformType?.let { apiType ->
- OpponentChatBubble(
- modifier = Modifier
- .padding(horizontal = 8.dp, vertical = 12.dp)
- .widthIn(max = maximumChatBubbleWidth),
- canRetry = canUseChat && isIdle && key >= latestMessageIndex,
- isLoading = false,
- apiType = apiType,
- text = m.content,
- onCopyClick = { clipboardManager.setText(AnnotatedString(m.content.trim())) },
- onRetryClick = { chatViewModel.retryQuestion(m) }
- )
- }
- }
- Spacer(modifier = Modifier.width(systemChatMargin))
- }
- }
- }
- }
-
- if (!isIdle) {
+ groupedMessages.userMessages.forEachIndexed { i, message ->
item {
- Row(
+ var isDropDownMenuExpanded by remember { mutableStateOf(false) }
+ Column(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 12.dp)
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalAlignment = Alignment.End
) {
- Spacer(modifier = Modifier.weight(1f))
- UserChatBubble(
- modifier = Modifier.widthIn(max = maximumChatBubbleWidth),
- text = userMessage.content,
- isLoading = true,
- onCopyClick = { clipboardManager.setText(AnnotatedString(userMessage.content.trim())) },
- onEditClick = { chatViewModel.openEditQuestionDialog(userMessage) }
- )
+ Box {
+ UserChatBubble(
+ modifier = Modifier.widthIn(max = maximumUserChatBubbleWidth),
+ text = message.content,
+ onLongPress = { isDropDownMenuExpanded = true }
+ )
+ ChatBubbleDropdownMenu(
+ isChatBubbleDropdownMenuExpanded = isDropDownMenuExpanded,
+ canEdit = canUseChat && isIdle,
+ onDismissRequest = { isDropDownMenuExpanded = false },
+ onEditItemClick = { chatViewModel.openEditQuestionDialog(message) },
+ onCopyItemClick = { clipboardManager.setText(AnnotatedString(message.content)) }
+ )
+ }
}
}
-
item {
- Row(
+ val platformIndexState = chatStates.indexStates[i]
+ val assistantContent = groupedMessages.assistantMessages[i][platformIndexState].content
+ val isLoading = chatStates.loadingStates[platformIndexState] == ChatViewModel.LoadingState.Loading
+
+ Column(
modifier = Modifier
.fillMaxWidth()
- .horizontalScroll(chatBubbleScrollStates[(latestMessageIndex + 1) / 2])
+ .padding(horizontal = 16.dp, vertical = 12.dp)
) {
- Spacer(modifier = Modifier.width(8.dp))
- chatViewModel.enabledPlatformsInChat.sorted().forEach { apiType ->
- val message = when (apiType) {
- ApiType.OPENAI -> openAIMessage
- ApiType.ANTHROPIC -> anthropicMessage
- ApiType.GOOGLE -> googleMessage
- ApiType.GROQ -> groqMessage
- ApiType.OLLAMA -> ollamaMessage
- }
-
- val loadingState = when (apiType) {
- ApiType.OPENAI -> openaiLoadingState
- ApiType.ANTHROPIC -> anthropicLoadingState
- ApiType.GOOGLE -> googleLoadingState
- ApiType.GROQ -> groqLoadingState
- ApiType.OLLAMA -> ollamaLoadingState
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ GPTMobileIcon(if (i == groupedMessages.assistantMessages.size - 1) !isIdle else false)
+ if (chatViewModel.enabledPlatformsInChat.size > 1) {
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState())
+ ) {
+ chatViewModel.enabledPlatformsInChat.forEachIndexed { j, uid ->
+ val platform = appEnabledPlatforms.find { it.uid == uid }
+ PlatformButton(
+ isLoading = if (i == groupedMessages.assistantMessages.size - 1) isLoading else false,
+ name = platform?.name ?: stringResource(R.string.unknown),
+ selected = platformIndexState == j,
+ onPlatformClick = { chatViewModel.updateChatPlatformIndex(i, j) }
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ }
+ }
}
-
- OpponentChatBubble(
- modifier = Modifier
- .padding(horizontal = 8.dp, vertical = 12.dp)
- .widthIn(max = maximumChatBubbleWidth),
- canRetry = canUseChat,
- isLoading = loadingState == ChatViewModel.LoadingState.Loading,
- apiType = apiType,
- text = message.content,
- onCopyClick = { clipboardManager.setText(AnnotatedString(message.content.trim())) },
- onRetryClick = { chatViewModel.retryQuestion(message) }
- )
}
- Spacer(modifier = Modifier.width(systemChatMargin))
+ OpponentChatBubble(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp)
+ .widthIn(max = maximumOpponentChatBubbleWidth),
+ canRetry = canUseChat && isIdle && i == groupedMessages.assistantMessages.size - 1,
+ isLoading = if (i == groupedMessages.assistantMessages.size - 1) isLoading else false,
+ text = assistantContent,
+ onCopyClick = { clipboardManager.setText(AnnotatedString(assistantContent)) },
+ onSelectClick = { chatViewModel.openSelectTextSheet(assistantContent) },
+ onRetryClick = {
+ // TODO()
+ }
+ )
}
}
}
@@ -310,66 +249,39 @@ fun ChatScreen(
if (isChatTitleDialogOpen) {
ChatTitleDialog(
initialTitle = chatRoom.title,
- aiCoreModeEnabled = false,
- aiGeneratedResult = geminiNano.content,
- isAICoreLoading = geminiNanoLoadingState == ChatViewModel.LoadingState.Loading,
onDefaultTitleMode = chatViewModel::generateDefaultChatTitle,
- onAICoreTitleMode = chatViewModel::generateAIChatTitle,
- onRetryRequest = chatViewModel::generateAIChatTitle,
onConfirmRequest = { title -> chatViewModel.updateChatTitle(title) },
onDismissRequest = chatViewModel::closeChatTitleDialog
)
}
if (isEditQuestionDialogOpen) {
+ val editedQuestion by chatViewModel.editedQuestion.collectAsStateWithLifecycle()
ChatQuestionEditDialog(
initialQuestion = editedQuestion,
onDismissRequest = chatViewModel::closeEditQuestionDialog,
onConfirmRequest = { question ->
- chatViewModel.editQuestion(question)
+ // TODO()
+ // chatViewModel.editQuestion(question)
chatViewModel.closeEditQuestionDialog()
}
)
}
- }
-}
-
-private fun checkAICoreAvailability(aiCore: PackageInfo?, privateComputeServices: PackageInfo?): Boolean {
- aiCore ?: return false
- privateComputeServices ?: return false
- val privateComputeMinVersion = "1.0.release.658389993"
- val aiCoreCondition = aiCore.versionName?.contains("thirdpartyeap") == true
- val privateComputeCondition = (privateComputeServices.versionName ?: "").padEnd(privateComputeMinVersion.length, '0') > privateComputeMinVersion
-
- return aiCoreCondition && privateComputeCondition
-}
-
-private fun groupMessages(messages: List): HashMap> {
- val classifiedMessages = hashMapOf>()
- var counter = 0
-
- messages.sortedBy { it.createdAt }.forEach { message ->
- if (message.platformType == null) {
- if (classifiedMessages.containsKey(counter) || counter % 2 == 1) {
- counter++
- }
-
- classifiedMessages[counter] = mutableListOf(message)
- counter++
- } else {
- if (counter % 2 == 0) {
- counter++
- }
-
- if (classifiedMessages.containsKey(counter)) {
- classifiedMessages[counter]?.add(message)
- } else {
- classifiedMessages[counter] = mutableListOf(message)
+ if (isSelectTextSheetOpen) {
+ val selectedText by chatViewModel.selectedText.collectAsStateWithLifecycle()
+ ModalBottomSheet(onDismissRequest = chatViewModel::closeSelectTextSheet) {
+ SelectionContainer(
+ modifier = Modifier
+ .padding(24.dp)
+ .heightIn(min = 200.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text(selectedText)
+ }
}
}
}
- return classifiedMessages
}
@Composable
@@ -401,7 +313,7 @@ private fun ChatTopBar(
}
ChatDropdownMenu(
- isDropDownMenuExpanded = isDropDownMenuExpanded,
+ isDropdownMenuExpanded = isDropDownMenuExpanded,
isMenuItemEnabled = isMenuItemEnabled,
onDismissRequest = { isDropDownMenuExpanded = false },
onChatTitleItemClick = {
@@ -417,7 +329,7 @@ private fun ChatTopBar(
@Composable
fun ChatDropdownMenu(
- isDropDownMenuExpanded: Boolean,
+ isDropdownMenuExpanded: Boolean,
isMenuItemEnabled: Boolean,
onDismissRequest: () -> Unit,
onChatTitleItemClick: () -> Unit,
@@ -425,7 +337,7 @@ fun ChatDropdownMenu(
) {
DropdownMenu(
modifier = Modifier.wrapContentSize(),
- expanded = isDropDownMenuExpanded,
+ expanded = isDropdownMenuExpanded,
onDismissRequest = onDismissRequest
) {
DropdownMenuItem(
@@ -445,6 +357,49 @@ fun ChatDropdownMenu(
}
}
+@Composable
+fun ChatBubbleDropdownMenu(
+ isChatBubbleDropdownMenuExpanded: Boolean,
+ canEdit: Boolean,
+ onDismissRequest: () -> Unit,
+ onEditItemClick: () -> Unit,
+ onCopyItemClick: () -> Unit
+) {
+ DropdownMenu(
+ modifier = Modifier.wrapContentSize(),
+ expanded = isChatBubbleDropdownMenuExpanded,
+ onDismissRequest = onDismissRequest
+ ) {
+ DropdownMenuItem(
+ enabled = canEdit,
+ leadingIcon = {
+ Icon(
+ Icons.Outlined.Edit,
+ contentDescription = stringResource(R.string.edit)
+ )
+ },
+ text = { Text(text = stringResource(R.string.edit)) },
+ onClick = {
+ onEditItemClick.invoke()
+ onDismissRequest.invoke()
+ }
+ )
+ DropdownMenuItem(
+ leadingIcon = {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_copy),
+ contentDescription = stringResource(R.string.copy_text)
+ )
+ },
+ text = { Text(text = stringResource(R.string.copy_text)) },
+ onClick = {
+ onCopyItemClick.invoke()
+ onDismissRequest.invoke()
+ }
+ )
+ }
+}
+
private fun exportChat(context: Context, chatViewModel: ChatViewModel) {
try {
val (fileName, fileContent) = chatViewModel.exportChat()
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatViewModel.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatViewModel.kt
index 566afea6..efb8bf33 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatViewModel.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/chat/ChatViewModel.kt
@@ -5,15 +5,13 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoom
-import dev.chungjungsoo.gptmobile.data.database.entity.Message
-import dev.chungjungsoo.gptmobile.data.dto.ApiState
-import dev.chungjungsoo.gptmobile.data.model.ApiType
+import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoomV2
+import dev.chungjungsoo.gptmobile.data.database.entity.MessageV2
+import dev.chungjungsoo.gptmobile.data.database.entity.PlatformV2
import dev.chungjungsoo.gptmobile.data.repository.ChatRepository
import dev.chungjungsoo.gptmobile.data.repository.SettingRepository
-import dev.chungjungsoo.gptmobile.util.handleStates
+import dev.chungjungsoo.gptmobile.util.getPlatformName
import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -31,13 +29,24 @@ class ChatViewModel @Inject constructor(
data object Loading : LoadingState()
}
+ data class GroupedMessages(
+ val userMessages: List = listOf(),
+ val assistantMessages: List> = listOf()
+ )
+
+ data class ChatStates(
+ val indexStates: List = listOf(),
+ val loadingStates: List = listOf()
+ )
+
private val chatRoomId: Int = checkNotNull(savedStateHandle["chatRoomId"])
private val enabledPlatformString: String = checkNotNull(savedStateHandle["enabledPlatforms"])
- val enabledPlatformsInChat = enabledPlatformString.split(',').map { s -> ApiType.valueOf(s) }
+ val enabledPlatformsInChat = enabledPlatformString.split(',')
+
private val currentTimeStamp: Long
get() = System.currentTimeMillis() / 1000
- private val _chatRoom = MutableStateFlow(ChatRoom(id = -1, title = "", enabledPlatform = enabledPlatformsInChat))
+ private val _chatRoom = MutableStateFlow(ChatRoomV2(id = -1, title = "", enabledPlatform = enabledPlatformsInChat))
val chatRoom = _chatRoom.asStateFlow()
private val _isChatTitleDialogOpen = MutableStateFlow(false)
@@ -46,40 +55,32 @@ class ChatViewModel @Inject constructor(
private val _isEditQuestionDialogOpen = MutableStateFlow(false)
val isEditQuestionDialogOpen = _isEditQuestionDialogOpen.asStateFlow()
- // Enabled platforms list
- private val _enabledPlatformsInApp = MutableStateFlow(listOf())
- val enabledPlatformsInApp = _enabledPlatformsInApp.asStateFlow()
+ private val _isSelectTextSheetOpen = MutableStateFlow(false)
+ val isSelectTextSheetOpen = _isSelectTextSheetOpen.asStateFlow()
- // List of question & answers (User, Assistant)
- private val _messages = MutableStateFlow(listOf())
- val messages: StateFlow> = _messages.asStateFlow()
+ // Enabled platforms list in app
+ private val _enabledPlatformsInApp = MutableStateFlow(listOf())
+ val enabledPlatformsInApp = _enabledPlatformsInApp.asStateFlow()
// User input used for TextField
private val _question = MutableStateFlow("")
val question: StateFlow = _question.asStateFlow()
- // Used for passing user question to Edit User Message Dialog
- private val _editedQuestion = MutableStateFlow(Message(chatId = chatRoomId, content = "", platformType = null))
- val editedQuestion = _editedQuestion.asStateFlow()
-
- // Loading state for each platforms
- private val _openaiLoadingState = MutableStateFlow(LoadingState.Idle)
- val openaiLoadingState = _openaiLoadingState.asStateFlow()
-
- private val _anthropicLoadingState = MutableStateFlow(LoadingState.Idle)
- val anthropicLoadingState = _anthropicLoadingState.asStateFlow()
-
- private val _googleLoadingState = MutableStateFlow(LoadingState.Idle)
- val googleLoadingState = _googleLoadingState.asStateFlow()
+ // Chat messages currently in the chat room
+ private val _groupedMessages = MutableStateFlow(GroupedMessages())
+ val groupedMessages = _groupedMessages.asStateFlow()
- private val _groqLoadingState = MutableStateFlow(LoadingState.Idle)
- val groqLoadingState = _groqLoadingState.asStateFlow()
+ // Each chat states for assistant chat messages
+ private val _chatStates = MutableStateFlow(ChatStates())
+ val chatStates = _chatStates.asStateFlow()
- private val _ollamaLoadingState = MutableStateFlow(LoadingState.Idle)
- val ollamaLoadingState = _ollamaLoadingState.asStateFlow()
+ // Used for passing user question to Edit User Message Dialog
+ private val _editedQuestion = MutableStateFlow(MessageV2(chatId = chatRoomId, content = "", platformType = null))
+ val editedQuestion = _editedQuestion.asStateFlow()
- private val _geminiNanoLoadingState = MutableStateFlow(LoadingState.Idle)
- val geminiNanoLoadingState = _geminiNanoLoadingState.asStateFlow()
+ // Used for text data to show in SelectText Bottom Sheet
+ private val _selectedText = MutableStateFlow("")
+ val selectedText = _selectedText.asStateFlow()
// Total loading state. It should be updated if one of the loading state has changed.
// If all loading states are idle, this value should have `true`.
@@ -90,134 +91,63 @@ class ChatViewModel @Inject constructor(
private val _isLoaded = MutableStateFlow(false)
val isLoaded = _isLoaded.asStateFlow()
- // Currently active(chat completion) user input. This is used when user input is sent.
- private val _userMessage = MutableStateFlow(Message(chatId = chatRoomId, content = "", platformType = null))
- val userMessage = _userMessage.asStateFlow()
-
- // Currently active(chat completion) assistant output. This is used when data is received from the API.
- private val _openAIMessage = MutableStateFlow(Message(chatId = chatRoomId, content = "", platformType = ApiType.OPENAI))
- val openAIMessage = _openAIMessage.asStateFlow()
-
- private val _anthropicMessage = MutableStateFlow(Message(chatId = chatRoomId, content = "", platformType = ApiType.ANTHROPIC))
- val anthropicMessage = _anthropicMessage.asStateFlow()
-
- private val _googleMessage = MutableStateFlow(Message(chatId = chatRoomId, content = "", platformType = ApiType.GOOGLE))
- val googleMessage = _googleMessage.asStateFlow()
-
- private val _groqMessage = MutableStateFlow(Message(chatId = chatRoomId, content = "", platformType = ApiType.GROQ))
- val groqMessage = _groqMessage.asStateFlow()
-
- private val _ollamaMessage = MutableStateFlow(Message(chatId = chatRoomId, content = "", platformType = ApiType.OLLAMA))
- val ollamaMessage = _ollamaMessage.asStateFlow()
-
- private val _geminiNanoMessage = MutableStateFlow(Message(chatId = chatRoomId, content = "", platformType = null))
- val geminiNanoMessage = _geminiNanoMessage.asStateFlow()
-
- // Flows for assistant message streams
- private val openAIFlow = MutableSharedFlow()
- private val anthropicFlow = MutableSharedFlow()
- private val googleFlow = MutableSharedFlow()
- private val groqFlow = MutableSharedFlow()
- private val ollamaFlow = MutableSharedFlow()
- private val geminiNanoFlow = MutableSharedFlow()
-
init {
Log.d("ViewModel", "$chatRoomId")
Log.d("ViewModel", "$enabledPlatformsInChat")
fetchChatRoom()
viewModelScope.launch { fetchMessages() }
fetchEnabledPlatformsInApp()
- observeFlow()
+ }
+
+ fun addMessage(userMessage: MessageV2) {
+ _groupedMessages.update {
+ it.copy(
+ userMessages = it.userMessages + listOf(userMessage),
+ assistantMessages = it.assistantMessages + listOf(
+ enabledPlatformsInChat.map { MessageV2(chatId = chatRoomId, content = "", platformType = it) }
+ )
+ )
+ }
+ _chatStates.update { it.copy(indexStates = it.indexStates + listOf(0)) }
}
fun askQuestion() {
Log.d("Question: ", _question.value)
- _userMessage.update { it.copy(content = _question.value, createdAt = currentTimeStamp) }
+ MessageV2(
+ chatId = chatRoomId,
+ content = _question.value,
+ platformType = null,
+ createdAt = currentTimeStamp
+ ).let { addMessage(it) }
_question.update { "" }
- completeChat()
+ // completeChat() TODO() : Complete the function
}
fun closeChatTitleDialog() = _isChatTitleDialogOpen.update { false }
fun closeEditQuestionDialog() {
- _editedQuestion.update { Message(chatId = chatRoomId, content = "", platformType = null) }
+ _editedQuestion.update { MessageV2(chatId = chatRoomId, content = "", platformType = null) }
_isEditQuestionDialogOpen.update { false }
}
- fun editQuestion(q: Message) {
- _messages.update { it.filter { message -> message.id < q.id && message.createdAt < q.createdAt } }
- _userMessage.update { it.copy(content = q.content, createdAt = currentTimeStamp) }
- completeChat()
+ fun closeSelectTextSheet() {
+ _isSelectTextSheetOpen.update { false }
+ _selectedText.update { "" }
}
fun openChatTitleDialog() = _isChatTitleDialogOpen.update { true }
- fun openEditQuestionDialog(question: Message) {
+ fun openEditQuestionDialog(question: MessageV2) {
_editedQuestion.update { question }
_isEditQuestionDialogOpen.update { true }
}
- fun generateDefaultChatTitle(): String? = chatRepository.generateDefaultChatTitle(_messages.value)
-
- fun generateAIChatTitle() {
- viewModelScope.launch {
- _geminiNanoLoadingState.update { LoadingState.Loading }
- _geminiNanoMessage.update { it.copy(content = "") }
- }
+ fun openSelectTextSheet(content: String) {
+ _selectedText.update { content }
+ _isSelectTextSheetOpen.update { true }
}
- fun retryQuestion(message: Message) {
- val latestQuestionIndex = _messages.value.indexOfLast { it.platformType == null }
-
- if (latestQuestionIndex != -1 && _isIdle.value) {
- // Update user input to latest question
- _userMessage.update { _messages.value[latestQuestionIndex] }
-
- // Get previous answers from the assistant
- val previousAnswers = enabledPlatformsInChat.mapNotNull { apiType -> _messages.value.lastOrNull { it.platformType == apiType } }
-
- // Remove latest question & answers
- _messages.update { it - setOf(_messages.value[latestQuestionIndex]) - previousAnswers.toSet() }
-
- // Restore messages that are not retrying
- enabledPlatformsInChat.forEach { apiType ->
- when (apiType) {
- message.platformType -> {}
- else -> restoreMessageState(apiType, previousAnswers)
- }
- }
- }
- message.platformType?.let { updateLoadingState(it, LoadingState.Loading) }
-
- when (message.platformType) {
- ApiType.OPENAI -> {
- _openAIMessage.update { it.copy(id = message.id, content = "", createdAt = currentTimeStamp) }
- completeOpenAIChat()
- }
-
- ApiType.ANTHROPIC -> {
- _anthropicMessage.update { it.copy(id = message.id, content = "", createdAt = currentTimeStamp) }
- completeAnthropicChat()
- }
-
- ApiType.GOOGLE -> {
- _googleMessage.update { it.copy(id = message.id, content = "", createdAt = currentTimeStamp) }
- completeGoogleChat()
- }
-
- ApiType.GROQ -> {
- _groqMessage.update { it.copy(id = message.id, content = "", createdAt = currentTimeStamp) }
- completeGroqChat()
- }
-
- ApiType.OLLAMA -> {
- _ollamaMessage.update { it.copy(id = message.id, content = "", createdAt = currentTimeStamp) }
- completeOllamaChat()
- }
-
- else -> {}
- }
- }
+ fun generateDefaultChatTitle(): String? = chatRepository.generateDefaultChatTitle(_groupedMessages.value.userMessages)
fun updateChatTitle(title: String) {
// Should be only used for changing chat title after the chatroom is created.
@@ -229,6 +159,19 @@ class ChatViewModel @Inject constructor(
}
}
+ fun updateChatPlatformIndex(assistantIndex: Int, platformIndex: Int) {
+ if (assistantIndex >= _chatStates.value.indexStates.size || assistantIndex < 0) return
+ if (platformIndex >= enabledPlatformsInChat.size || platformIndex < 0) return
+
+ _chatStates.update {
+ val updatedIndex = it.indexStates.toMutableList()
+ updatedIndex[assistantIndex] = platformIndex
+ it.copy(indexStates = updatedIndex)
+ }
+ }
+
+ fun updateQuestion(q: String) = _question.update { q }
+
fun exportChat(): Pair {
// Build the chat history in Markdown format
val chatHistoryMarkdown = buildString {
@@ -240,11 +183,16 @@ class ChatViewModel @Inject constructor(
appendLine()
appendLine("## Chat History")
appendLine()
- messages.value.forEach { message ->
- val sender = if (message.platformType == null) "User" else "Assistant"
- appendLine("**$sender:**")
+ _groupedMessages.value.userMessages.forEachIndexed { i, message ->
+ appendLine("**User:**")
appendLine(message.content)
appendLine()
+
+ _groupedMessages.value.assistantMessages[i].forEach { message ->
+ appendLine("**Assistant (${_enabledPlatformsInApp.value.getPlatformName(message.platformType!!)}):**")
+ appendLine(message.content)
+ appendLine()
+ }
}
}
@@ -259,242 +207,72 @@ class ChatViewModel @Inject constructor(
return format.format(currentDate)
}
- fun updateQuestion(q: String) = _question.update { q }
-
- private fun addMessage(message: Message) = _messages.update { it + listOf(message) }
-
- private fun clearQuestionAndAnswers() {
- _userMessage.update { it.copy(id = 0, content = "") }
- _openAIMessage.update { it.copy(id = 0, content = "") }
- _anthropicMessage.update { it.copy(id = 0, content = "") }
- _googleMessage.update { it.copy(id = 0, content = "") }
- _groqMessage.update { it.copy(id = 0, content = "") }
- _ollamaMessage.update { it.copy(id = 0, content = "") }
- }
-
- private fun completeChat() {
- enabledPlatformsInChat.forEach { apiType -> updateLoadingState(apiType, LoadingState.Loading) }
- val enabledPlatforms = enabledPlatformsInChat.toSet()
-
- if (ApiType.OPENAI in enabledPlatforms) {
- completeOpenAIChat()
- }
-
- if (ApiType.ANTHROPIC in enabledPlatforms) {
- completeAnthropicChat()
- }
-
- if (ApiType.GOOGLE in enabledPlatforms) {
- completeGoogleChat()
- }
-
- if (ApiType.GROQ in enabledPlatforms) {
- completeGroqChat()
- }
-
- if (ApiType.OLLAMA in enabledPlatforms) {
- completeOllamaChat()
- }
- }
-
- private fun completeAnthropicChat() {
- viewModelScope.launch {
- val chatFlow = chatRepository.completeAnthropicChat(question = _userMessage.value, history = _messages.value)
- chatFlow.collect { chunk -> anthropicFlow.emit(chunk) }
- }
- }
-
- private fun completeGoogleChat() {
- viewModelScope.launch {
- val chatFlow = chatRepository.completeGoogleChat(question = _userMessage.value, history = _messages.value)
- chatFlow.collect { chunk -> googleFlow.emit(chunk) }
- }
- }
-
- private fun completeGroqChat() {
- viewModelScope.launch {
- val chatFlow = chatRepository.completeGroqChat(question = _userMessage.value, history = _messages.value)
- chatFlow.collect { chunk -> groqFlow.emit(chunk) }
- }
- }
-
- private fun completeOllamaChat() {
- viewModelScope.launch {
- val chatFlow = chatRepository.completeOllamaChat(question = _userMessage.value, history = _messages.value)
- chatFlow.collect { chunk -> ollamaFlow.emit(chunk) }
- }
- }
-
- private fun completeOpenAIChat() {
- viewModelScope.launch {
- val chatFlow = chatRepository.completeOpenAIChat(question = _userMessage.value, history = _messages.value)
- chatFlow.collect { chunk -> openAIFlow.emit(chunk) }
- }
- }
-
private suspend fun fetchMessages() {
// If the room isn't new
if (chatRoomId != 0) {
- _messages.update { chatRepository.fetchMessages(chatRoomId) }
+ _groupedMessages.update { fetchGroupedMessages(chatRoomId) }
+ _chatStates.update {
+ it.copy(
+ indexStates = List(_groupedMessages.value.assistantMessages.size) { 0 },
+ loadingStates = List(enabledPlatformsInChat.size) { LoadingState.Idle }
+ )
+ }
_isLoaded.update { true } // Finish fetching
return
}
// When message id should sync after saving chats
if (_chatRoom.value.id != 0) {
- _messages.update { chatRepository.fetchMessages(_chatRoom.value.id) }
+ _groupedMessages.update { fetchGroupedMessages(_chatRoom.value.id) }
return
}
}
- private fun fetchChatRoom() {
- viewModelScope.launch {
- _chatRoom.update {
- if (chatRoomId == 0) {
- ChatRoom(id = 0, title = "Untitled Chat", enabledPlatform = enabledPlatformsInChat)
- } else {
- chatRepository.fetchChatList().first { it.id == chatRoomId }
- }
- }
- Log.d("ViewModel", "chatroom: $chatRoom")
- }
- }
+ private suspend fun fetchGroupedMessages(chatId: Int): GroupedMessages {
+ val messages = chatRepository.fetchMessagesV2(chatId).sortedBy { it.createdAt }
+ val platformOrderMap = enabledPlatformsInChat.withIndex().associate { (idx, uuid) -> uuid to idx }
- private fun fetchEnabledPlatformsInApp() {
- viewModelScope.launch {
- val enabled = settingRepository.fetchPlatforms().filter { it.enabled }.map { it.name }
- _enabledPlatformsInApp.update { enabled }
- }
- }
+ val userMessages = mutableListOf()
+ val assistantMessages = mutableListOf>()
- private fun observeFlow() {
- viewModelScope.launch {
- openAIFlow.handleStates(
- messageFlow = _openAIMessage,
- onLoadingComplete = { updateLoadingState(ApiType.OPENAI, LoadingState.Idle) }
- )
- }
-
- viewModelScope.launch {
- anthropicFlow.handleStates(
- messageFlow = _anthropicMessage,
- onLoadingComplete = { updateLoadingState(ApiType.ANTHROPIC, LoadingState.Idle) }
- )
- }
-
- viewModelScope.launch {
- googleFlow.handleStates(
- messageFlow = _googleMessage,
- onLoadingComplete = { updateLoadingState(ApiType.GOOGLE, LoadingState.Idle) }
- )
- }
-
- viewModelScope.launch {
- groqFlow.handleStates(
- messageFlow = _groqMessage,
- onLoadingComplete = { updateLoadingState(ApiType.GROQ, LoadingState.Idle) }
- )
+ messages.forEach { message ->
+ if (message.platformType == null) {
+ userMessages.add(message)
+ assistantMessages.add(mutableListOf())
+ } else {
+ assistantMessages.last().add(message)
+ }
}
- viewModelScope.launch {
- ollamaFlow.handleStates(
- messageFlow = _ollamaMessage,
- onLoadingComplete = { updateLoadingState(ApiType.OLLAMA, LoadingState.Idle) }
+ val sortedAssistantMessages = assistantMessages.map {
+ it.sortedWith(
+ compareBy(
+ { platformOrderMap[it.platformType] ?: Int.MAX_VALUE },
+ { it.platformType }
+ )
)
}
- viewModelScope.launch {
- geminiNanoFlow.handleStates(
- messageFlow = _geminiNanoMessage,
- onLoadingComplete = { _geminiNanoLoadingState.update { LoadingState.Idle } }
- )
- }
+ return GroupedMessages(userMessages, sortedAssistantMessages)
+ }
+ private fun fetchChatRoom() {
viewModelScope.launch {
- _isIdle.collect { status ->
- if (status) {
- Log.d("status", "val: ${_userMessage.value}")
- if (_chatRoom.value.id != -1 && _userMessage.value.content.isNotBlank()) {
- syncQuestionAndAnswers()
- Log.d("message", "${_messages.value}")
- _chatRoom.update { chatRepository.saveChat(_chatRoom.value, _messages.value) }
- fetchMessages() // For syncing message ids
- }
- clearQuestionAndAnswers()
+ _chatRoom.update {
+ if (chatRoomId == 0) {
+ ChatRoomV2(id = 0, title = "Untitled Chat", enabledPlatform = enabledPlatformsInChat)
+ } else {
+ chatRepository.fetchChatListV2().first { it.id == chatRoomId }
}
}
+ Log.d("ViewModel", "chatroom: ${chatRoom.value}")
}
}
- private fun restoreMessageState(apiType: ApiType, previousAnswers: List) {
- val message = previousAnswers.firstOrNull { it.platformType == apiType }
- val retryingState = when (apiType) {
- ApiType.OPENAI -> _openaiLoadingState
- ApiType.ANTHROPIC -> _anthropicLoadingState
- ApiType.GOOGLE -> _googleLoadingState
- ApiType.GROQ -> _groqLoadingState
- ApiType.OLLAMA -> _ollamaLoadingState
- }
-
- if (retryingState == LoadingState.Loading) return
- if (message == null) return
-
- when (apiType) {
- ApiType.OPENAI -> _openAIMessage.update { message }
- ApiType.ANTHROPIC -> _anthropicMessage.update { message }
- ApiType.GOOGLE -> _googleMessage.update { message }
- ApiType.GROQ -> _groqMessage.update { message }
- ApiType.OLLAMA -> _ollamaMessage.update { message }
- }
- }
-
- private fun syncQuestionAndAnswers() {
- addMessage(_userMessage.value)
- val enabledPlatforms = enabledPlatformsInChat.toSet()
-
- if (ApiType.OPENAI in enabledPlatforms) {
- addMessage(_openAIMessage.value)
- }
-
- if (ApiType.ANTHROPIC in enabledPlatforms) {
- addMessage(_anthropicMessage.value)
- }
-
- if (ApiType.GOOGLE in enabledPlatforms) {
- addMessage(_googleMessage.value)
- }
-
- if (ApiType.GROQ in enabledPlatforms) {
- addMessage(_groqMessage.value)
- }
-
- if (ApiType.OLLAMA in enabledPlatforms) {
- addMessage(_ollamaMessage.value)
- }
- }
-
- private fun updateLoadingState(apiType: ApiType, loadingState: LoadingState) {
- when (apiType) {
- ApiType.OPENAI -> _openaiLoadingState.update { loadingState }
- ApiType.ANTHROPIC -> _anthropicLoadingState.update { loadingState }
- ApiType.GOOGLE -> _googleLoadingState.update { loadingState }
- ApiType.GROQ -> _groqLoadingState.update { loadingState }
- ApiType.OLLAMA -> _ollamaLoadingState.update { loadingState }
- }
-
- var result = true
- enabledPlatformsInChat.forEach {
- val state = when (it) {
- ApiType.OPENAI -> _openaiLoadingState
- ApiType.ANTHROPIC -> _anthropicLoadingState
- ApiType.GOOGLE -> _googleLoadingState
- ApiType.GROQ -> _groqLoadingState
- ApiType.OLLAMA -> _ollamaLoadingState
- }
-
- result = result && (state.value is LoadingState.Idle)
+ private fun fetchEnabledPlatformsInApp() {
+ viewModelScope.launch {
+ val filtered = settingRepository.fetchPlatformV2s().filter { it.enabled }
+ _enabledPlatformsInApp.update { filtered }
}
-
- _isIdle.update { result }
}
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/home/HomeScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/home/HomeScreen.kt
index fdc8204d..6f828bff 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/home/HomeScreen.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/home/HomeScreen.kt
@@ -65,21 +65,19 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chungjungsoo.gptmobile.R
-import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoom
-import dev.chungjungsoo.gptmobile.data.dto.Platform
-import dev.chungjungsoo.gptmobile.data.model.ApiType
+import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoomV2
+import dev.chungjungsoo.gptmobile.data.database.entity.PlatformV2
import dev.chungjungsoo.gptmobile.presentation.common.PlatformCheckBoxItem
-import dev.chungjungsoo.gptmobile.util.getPlatformTitleResources
+import dev.chungjungsoo.gptmobile.util.getPlatformName
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun HomeScreen(
homeViewModel: HomeViewModel = hiltViewModel(),
settingOnClick: () -> Unit,
- onExistingChatClick: (ChatRoom) -> Unit,
- navigateToNewChat: (enabledPlatforms: List) -> Unit
+ onExistingChatClick: (ChatRoomV2) -> Unit,
+ navigateToNewChat: (enabledPlatforms: List) -> Unit
) {
- val platformTitles = getPlatformTitleResources()
val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val chatListState by homeViewModel.chatListState.collectAsStateWithLifecycle()
@@ -107,7 +105,7 @@ fun HomeScreen(
topBar = {
HomeTopAppBar(
chatListState.isSelectionMode,
- selectedChats = chatListState.selected.count { it },
+ selectedChats = chatListState.selectedChats.count { it },
scrollBehavior,
actionOnClick = {
if (chatListState.isSelectionMode) {
@@ -139,7 +137,7 @@ fun HomeScreen(
) {
item { ChatsTitle(scrollBehavior) }
itemsIndexed(chatListState.chats, key = { _, it -> it.id }) { idx, chatRoom ->
- val usingPlatform = chatRoom.enabledPlatform.joinToString(", ") { platformTitles[it] ?: "" }
+ val usingPlatform = chatRoom.enabledPlatform.map { uid -> platformState.getPlatformName(uid) }.joinToString(", ")
ListItem(
modifier = Modifier
.fillMaxWidth()
@@ -162,7 +160,7 @@ fun HomeScreen(
leadingContent = {
if (chatListState.isSelectionMode) {
Checkbox(
- checked = chatListState.selected[idx],
+ checked = chatListState.selectedChats[idx],
onCheckedChange = { homeViewModel.selectChat(idx) }
)
} else {
@@ -180,12 +178,13 @@ fun HomeScreen(
if (showSelectModelDialog) {
SelectPlatformDialog(
platformState,
+ selectedPlatforms = chatListState.selectedPlatforms,
onDismissRequest = { homeViewModel.closeSelectModelDialog() },
onConfirmation = {
- homeViewModel.closeSelectModelDialog()
navigateToNewChat(it)
+ homeViewModel.closeSelectModelDialog()
},
- onPlatformSelect = { homeViewModel.updateCheckedState(it) }
+ onPlatformSelect = { homeViewModel.updatePlatformCheckedState(it) }
)
}
@@ -193,7 +192,7 @@ fun HomeScreen(
DeleteWarningDialog(
onDismissRequest = homeViewModel::closeDeleteWarningDialog,
onConfirm = {
- val deletedChatRoomCount = chatListState.selected.count { it }
+ val deletedChatRoomCount = chatListState.selectedChats.count { it }
homeViewModel.deleteSelectedChats()
Toast.makeText(context, context.getString(R.string.deleted_chats, deletedChatRoomCount), Toast.LENGTH_SHORT).show()
homeViewModel.closeDeleteWarningDialog()
@@ -331,12 +330,12 @@ fun NewChatButton(
@Composable
fun SelectPlatformDialog(
- platforms: List,
+ platforms: List,
+ selectedPlatforms: List,
onDismissRequest: () -> Unit,
- onConfirmation: (enabledPlatforms: List) -> Unit,
- onPlatformSelect: (Platform) -> Unit
+ onConfirmation: (enabledPlatforms: List) -> Unit,
+ onPlatformSelect: (idx: Int) -> Unit
) {
- val titles = getPlatformTitleResources()
val configuration = LocalConfiguration.current
AlertDialog(
@@ -363,13 +362,13 @@ fun SelectPlatformDialog(
HorizontalDivider()
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
if (platforms.any { it.enabled }) {
- platforms.forEach { platform ->
+ platforms.forEachIndexed { i, platform ->
PlatformCheckBoxItem(
- platform = platform,
- title = titles[platform.name]!!,
+ title = platform.name,
enabled = platform.enabled,
+ selected = selectedPlatforms[i],
description = null,
- onClickEvent = { onPlatformSelect(platform) }
+ onClickEvent = { onPlatformSelect(i) }
)
}
} else {
@@ -380,8 +379,8 @@ fun SelectPlatformDialog(
},
confirmButton = {
TextButton(
- enabled = platforms.any { it.selected },
- onClick = { onConfirmation(platforms.filter { it.selected }.map { it.name }) }
+ enabled = selectedPlatforms.any { it },
+ onClick = { onConfirmation(platforms.filterIndexed { i, _ -> selectedPlatforms[i] }.map { it.uid }) }
) {
Text(stringResource(R.string.confirm))
}
@@ -410,24 +409,6 @@ fun EnablePlatformWarningText() {
)
}
-@Preview
-@Composable
-private fun SelectPlatformDialogPreview() {
- val platforms = listOf(
- Platform(ApiType.OPENAI, enabled = true),
- Platform(ApiType.ANTHROPIC, enabled = false),
- Platform(ApiType.GOOGLE, enabled = false),
- Platform(ApiType.GROQ, enabled = true),
- Platform(ApiType.OLLAMA, enabled = true)
- )
- SelectPlatformDialog(
- platforms = platforms,
- onDismissRequest = {},
- onConfirmation = {},
- onPlatformSelect = {}
- )
-}
-
@Composable
fun DeleteWarningDialog(
onDismissRequest: () -> Unit,
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/home/HomeViewModel.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/home/HomeViewModel.kt
index 0f22c2c9..336ccdcc 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/home/HomeViewModel.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/home/HomeViewModel.kt
@@ -4,8 +4,8 @@ import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoom
-import dev.chungjungsoo.gptmobile.data.dto.Platform
+import dev.chungjungsoo.gptmobile.data.database.entity.ChatRoomV2
+import dev.chungjungsoo.gptmobile.data.database.entity.PlatformV2
import dev.chungjungsoo.gptmobile.data.repository.ChatRepository
import dev.chungjungsoo.gptmobile.data.repository.SettingRepository
import javax.inject.Inject
@@ -22,16 +22,17 @@ class HomeViewModel @Inject constructor(
) : ViewModel() {
data class ChatListState(
- val chats: List = listOf(),
+ val chats: List = listOf(),
val isSelectionMode: Boolean = false,
- val selected: List = listOf()
+ val selectedPlatforms: List = listOf(),
+ val selectedChats: List = listOf()
)
private val _chatListState = MutableStateFlow(ChatListState())
val chatListState: StateFlow = _chatListState.asStateFlow()
- private val _platformState = MutableStateFlow(listOf())
- val platformState: StateFlow> = _platformState.asStateFlow()
+ private val _platformState = MutableStateFlow(listOf())
+ val platformState = _platformState.asStateFlow()
private val _showSelectModelDialog = MutableStateFlow(false)
val showSelectModelDialog: StateFlow = _showSelectModelDialog.asStateFlow()
@@ -39,19 +40,19 @@ class HomeViewModel @Inject constructor(
private val _showDeleteWarningDialog = MutableStateFlow(false)
val showDeleteWarningDialog: StateFlow = _showDeleteWarningDialog.asStateFlow()
- fun updateCheckedState(platform: Platform) {
- val index = _platformState.value.indexOf(platform)
+ fun updatePlatformCheckedState(idx: Int) {
+ if (idx < 0 || idx >= _chatListState.value.selectedPlatforms.size) return
- if (index >= 0) {
- _platformState.update {
- it.mapIndexed { i, p ->
- if (index == i) {
- p.copy(selected = p.selected.not())
+ _chatListState.update {
+ it.copy(
+ selectedPlatforms = it.selectedPlatforms.mapIndexed { index, b ->
+ if (index == idx) {
+ !b
} else {
- p
+ b
}
}
- }
+ )
}
}
@@ -71,16 +72,17 @@ class HomeViewModel @Inject constructor(
fun closeSelectModelDialog() {
_showSelectModelDialog.update { false }
+ _chatListState.update { it.copy(selectedPlatforms = List(it.selectedPlatforms.size) { false }) }
}
fun deleteSelectedChats() {
viewModelScope.launch {
val selectedChats = _chatListState.value.chats.filterIndexed { index, _ ->
- _chatListState.value.selected[index]
+ _chatListState.value.selectedChats[index]
}
- chatRepository.deleteChats(selectedChats)
- _chatListState.update { it.copy(chats = chatRepository.fetchChatList()) }
+ chatRepository.deleteChatsV2(selectedChats)
+ _chatListState.update { it.copy(chats = chatRepository.fetchChatListV2()) }
disableSelectionMode()
}
}
@@ -88,7 +90,7 @@ class HomeViewModel @Inject constructor(
fun disableSelectionMode() {
_chatListState.update {
it.copy(
- selected = List(it.chats.size) { false },
+ selectedChats = List(it.chats.size) { false },
isSelectionMode = false
)
}
@@ -100,12 +102,12 @@ class HomeViewModel @Inject constructor(
fun fetchChats() {
viewModelScope.launch {
- val chats = chatRepository.fetchChatList()
+ val chats = chatRepository.fetchChatListV2()
_chatListState.update {
it.copy(
chats = chats,
- selected = List(chats.size) { false },
+ selectedChats = List(chats.size) { false },
isSelectionMode = false
)
}
@@ -116,8 +118,12 @@ class HomeViewModel @Inject constructor(
fun fetchPlatformStatus() {
viewModelScope.launch {
- val platforms = settingRepository.fetchPlatforms()
+ val platforms = settingRepository.fetchPlatformV2s()
_platformState.update { platforms }
+
+ if (_chatListState.value.selectedPlatforms.size != platforms.size) {
+ _chatListState.update { it.copy(selectedPlatforms = List(platforms.size) { false }) }
+ }
}
}
@@ -126,7 +132,7 @@ class HomeViewModel @Inject constructor(
_chatListState.update {
it.copy(
- selected = it.selected.mapIndexed { index, b ->
+ selectedChats = it.selectedChats.mapIndexed { index, b ->
if (index == chatRoomIdx) {
!b
} else {
@@ -136,7 +142,7 @@ class HomeViewModel @Inject constructor(
)
}
- if (_chatListState.value.selected.count { it } == 0) {
+ if (_chatListState.value.selectedChats.count { it } == 0) {
disableSelectionMode()
}
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/main/MainActivity.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/main/MainActivity.kt
index 877cfd7e..b9a5577a 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/main/MainActivity.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/main/MainActivity.kt
@@ -53,10 +53,20 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
mainViewModel.event.collect { event ->
- if (event == MainViewModel.SplashEvent.OpenIntro) {
- navigate(Route.GET_STARTED) {
- popUpTo(Route.CHAT_LIST) { inclusive = true }
+ when (event) {
+ MainViewModel.SplashEvent.OpenIntro -> {
+ navigate(Route.GET_STARTED) {
+ popUpTo(Route.CHAT_LIST) { inclusive = true }
+ }
}
+
+ MainViewModel.SplashEvent.OpenMigrate -> {
+ navigate(Route.MIGRATE_V2) {
+ popUpTo(Route.CHAT_LIST) { inclusive = true }
+ }
+ }
+
+ else -> {}
}
}
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/main/MainViewModel.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/main/MainViewModel.kt
index 0a80a755..2044da31 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/main/MainViewModel.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/main/MainViewModel.kt
@@ -20,6 +20,7 @@ class MainViewModel @Inject constructor(private val settingRepository: SettingRe
sealed class SplashEvent {
data object OpenIntro : SplashEvent()
data object OpenHome : SplashEvent()
+ data object OpenMigrate : SplashEvent()
}
private val _isReady: MutableStateFlow = MutableStateFlow(false)
@@ -31,12 +32,24 @@ class MainViewModel @Inject constructor(private val settingRepository: SettingRe
init {
viewModelScope.launch {
val platforms = settingRepository.fetchPlatforms()
+ val platformV2s = settingRepository.fetchPlatformV2s()
- if (platforms.all { it.enabled.not() }) {
- // Initialize
- sendSplashEvent(SplashEvent.OpenIntro)
- } else {
- sendSplashEvent(SplashEvent.OpenHome)
+ when {
+ (platforms.all { it.enabled.not() } && platforms.all { it.token == null }) &&
+ (platformV2s.isEmpty())
+ -> {
+ // Initialize
+ sendSplashEvent(SplashEvent.OpenIntro)
+ }
+
+ platformV2s.isEmpty() -> {
+ // Migrate to V2
+ sendSplashEvent(SplashEvent.OpenMigrate)
+ }
+
+ else -> {
+ sendSplashEvent(SplashEvent.OpenHome)
+ }
}
setAsReady()
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/migrate/MigrateScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/migrate/MigrateScreen.kt
new file mode 100644
index 00000000..f5736111
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/migrate/MigrateScreen.kt
@@ -0,0 +1,193 @@
+package dev.chungjungsoo.gptmobile.presentation.ui.migrate
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.heading
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import dev.chungjungsoo.gptmobile.R
+import dev.chungjungsoo.gptmobile.presentation.common.PrimaryLongButton
+import dev.chungjungsoo.gptmobile.presentation.icons.Block
+import dev.chungjungsoo.gptmobile.presentation.icons.Complete
+import dev.chungjungsoo.gptmobile.presentation.icons.Error
+import dev.chungjungsoo.gptmobile.presentation.icons.Migrating
+import dev.chungjungsoo.gptmobile.presentation.icons.Ready
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MigrateScreen(
+ modifier: Modifier = Modifier,
+ migrateViewModel: MigrateViewModel = hiltViewModel(),
+ onFinish: () -> Unit
+) {
+ val uiState by migrateViewModel.uiState.collectAsStateWithLifecycle()
+
+ Scaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = { TopAppBar(title = {}) }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ ) {
+ MigrationTitle()
+ PlatformMigrationCard(
+ status = uiState.platformState,
+ numberOfPlatforms = uiState.numberOfPlatforms,
+ onMigrationClick = migrateViewModel::migratePlatform
+ )
+ ChatRoomMessageMigrationCard(
+ status = uiState.chatState,
+ numberOfChats = uiState.numberOfChats,
+ onMigrationClick = migrateViewModel::migrateChats
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ PrimaryLongButton(
+ enabled = uiState.platformState == MigrateViewModel.MigrationState.MIGRATED &&
+ uiState.chatState == MigrateViewModel.MigrationState.MIGRATED,
+ onClick = onFinish,
+ text = stringResource(R.string.done)
+ )
+ }
+ }
+}
+
+@Composable
+fun MigrationTitle(modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(4.dp)
+ .semantics { heading() },
+ text = stringResource(R.string.migration_assistant),
+ style = MaterialTheme.typography.headlineMedium
+ )
+ Text(
+ modifier = Modifier.padding(4.dp),
+ text = stringResource(R.string.migration_description),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+}
+
+@Composable
+fun MigrationCard(
+ status: MigrateViewModel.MigrationState,
+ title: @Composable String,
+ description: @Composable String,
+ onMigrationClick: () -> Unit
+) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 8.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxHeight(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = when (status) {
+ MigrateViewModel.MigrationState.READY -> Ready
+ MigrateViewModel.MigrationState.MIGRATING -> Migrating
+ MigrateViewModel.MigrationState.MIGRATED -> Complete
+ MigrateViewModel.MigrationState.ERROR -> Error
+ MigrateViewModel.MigrationState.BLOCKED -> Block
+ },
+ contentDescription = null,
+ modifier = Modifier.size(48.dp)
+ )
+ Column {
+ Text(
+ text = title,
+ modifier = Modifier
+ .padding(start = 16.dp, end = 16.dp, top = 16.dp),
+ textAlign = TextAlign.Center
+ )
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier
+ .padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
+ textAlign = TextAlign.Center
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ TextButton(
+ onClick = onMigrationClick,
+ enabled = status == MigrateViewModel.MigrationState.READY || status == MigrateViewModel.MigrationState.ERROR
+ ) {
+ when (status) {
+ MigrateViewModel.MigrationState.READY, MigrateViewModel.MigrationState.BLOCKED -> Text(stringResource(R.string.migrate))
+ MigrateViewModel.MigrationState.MIGRATING -> Text(stringResource(R.string.migrating))
+ MigrateViewModel.MigrationState.MIGRATED -> Text(stringResource(R.string.migrated))
+ MigrateViewModel.MigrationState.ERROR -> Text(stringResource(R.string.error))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun PlatformMigrationCard(
+ status: MigrateViewModel.MigrationState,
+ numberOfPlatforms: Int,
+ onMigrationClick: () -> Unit
+) {
+ MigrationCard(
+ status = status,
+ title = stringResource(R.string.migrate_platform),
+ description = stringResource(R.string.enabled_platform_numbers, numberOfPlatforms),
+ onMigrationClick = onMigrationClick
+ )
+}
+
+@Composable
+fun ChatRoomMessageMigrationCard(
+ status: MigrateViewModel.MigrationState,
+ numberOfChats: Int,
+ onMigrationClick: () -> Unit
+) {
+ MigrationCard(
+ status = status,
+ title = stringResource(R.string.migrate_chat),
+ description = stringResource(R.string.existing_chats, numberOfChats),
+ onMigrationClick = onMigrationClick
+ )
+}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/migrate/MigrateViewModel.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/migrate/MigrateViewModel.kt
new file mode 100644
index 00000000..f544f209
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/migrate/MigrateViewModel.kt
@@ -0,0 +1,85 @@
+package dev.chungjungsoo.gptmobile.presentation.ui.migrate
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dev.chungjungsoo.gptmobile.data.repository.ChatRepository
+import dev.chungjungsoo.gptmobile.data.repository.SettingRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class MigrateViewModel @Inject constructor(
+ private val settingRepository: SettingRepository,
+ private val chatRepository: ChatRepository
+) : ViewModel() {
+ enum class MigrationState {
+ READY,
+ MIGRATING,
+ MIGRATED,
+ ERROR,
+ BLOCKED
+ }
+
+ data class MigrationUIState(
+ val platformState: MigrationState = MigrationState.READY,
+ val chatState: MigrationState = MigrationState.BLOCKED,
+ val numberOfPlatforms: Int = 0,
+ val numberOfChats: Int = 0
+ )
+
+ private val _uiState = MutableStateFlow(MigrationUIState())
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ updateAvailableMigrations()
+ }
+
+ fun migratePlatform() {
+ viewModelScope.launch {
+ try {
+ _uiState.update { it.copy(platformState = MigrationState.MIGRATING) }
+ settingRepository.migrateToPlatformV2()
+ _uiState.update {
+ it.copy(
+ platformState = MigrationState.MIGRATED,
+ chatState = MigrationState.READY
+ )
+ }
+ } catch (e: Exception) {
+ _uiState.update { it.copy(platformState = MigrationState.ERROR) }
+ Log.e("Migration", "Error migrating platform", e)
+ }
+ }
+ }
+
+ fun migrateChats() {
+ viewModelScope.launch {
+ try {
+ _uiState.update { it.copy(chatState = MigrationState.MIGRATING) }
+ chatRepository.migrateToChatRoomV2MessageV2()
+ _uiState.update { it.copy(chatState = MigrationState.MIGRATED) }
+ } catch (e: Exception) {
+ _uiState.update { it.copy(chatState = MigrationState.ERROR) }
+ Log.e("Migration", "Error migrating platform", e)
+ }
+ }
+ }
+
+ private fun updateAvailableMigrations() {
+ viewModelScope.launch {
+ val numberOfPlatforms = settingRepository.fetchPlatforms().filter { it.enabled }.size
+ val numberOfChats = chatRepository.fetchChatList().size
+ _uiState.update {
+ it.copy(
+ numberOfPlatforms = numberOfPlatforms,
+ numberOfChats = numberOfChats
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt
index 926bc86f..faecd533 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingDialogs.kt
@@ -262,7 +262,6 @@ private fun APIKeyDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
- enabled = token.isNotBlank(),
onClick = { onConfirmRequest(token) }
) {
Text(stringResource(R.string.confirm))
@@ -359,7 +358,7 @@ private fun ModelDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
- enabled = if (customSelected) customModel.isNotBlank() else model.isNotBlank(),
+ enabled = customSelected || model.isNotBlank(),
onClick = {
if (customSelected) {
onConfirmRequest(customModel)
@@ -567,7 +566,6 @@ private fun SystemPromptDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
- enabled = textFieldPrompt.isNotBlank(),
onClick = { onConfirmRequest(textFieldPrompt) }
) {
Text(stringResource(R.string.confirm))
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt
index ce37336f..0ca37aaf 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/PlatformSettingScreen.kt
@@ -116,7 +116,7 @@ fun PlatformSettingScreen(
SettingItem(
modifier = Modifier.height(64.dp),
title = stringResource(R.string.api_key),
- description = token?.let { stringResource(R.string.token_set, it[0]) } ?: stringResource(R.string.token_not_set),
+ description = if (token.isNullOrEmpty()) stringResource(R.string.token_not_set) else stringResource(R.string.token_set, token[0]),
enabled = enabled,
onItemClick = settingViewModel::openApiTokenDialog,
showTrailingIcon = false,
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/SettingViewModel.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/SettingViewModel.kt
index 05c0d62f..f19507cd 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/SettingViewModel.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setting/SettingViewModel.kt
@@ -75,7 +75,7 @@ class SettingViewModel @Inject constructor(
if (index >= 0) {
_platformState.update {
it.mapIndexed { i, p ->
- if (index == i && token.isNotBlank()) {
+ if (index == i) {
p.copy(token = token)
} else {
p
@@ -144,7 +144,7 @@ class SettingViewModel @Inject constructor(
if (index >= 0) {
_platformState.update {
it.mapIndexed { i, p ->
- if (index == i && prompt.isNotBlank()) {
+ if (index == i) {
p.copy(systemPrompt = prompt)
} else {
p
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setup/SelectPlatformScreen.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setup/SelectPlatformScreen.kt
index ece667c2..297e7137 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setup/SelectPlatformScreen.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/presentation/ui/setup/SelectPlatformScreen.kt
@@ -22,7 +22,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chungjungsoo.gptmobile.R
import dev.chungjungsoo.gptmobile.data.dto.Platform
-import dev.chungjungsoo.gptmobile.presentation.common.PlatformCheckBoxItem
import dev.chungjungsoo.gptmobile.presentation.common.PrimaryLongButton
import dev.chungjungsoo.gptmobile.presentation.common.Route
import dev.chungjungsoo.gptmobile.util.getPlatformDescriptionResources
@@ -100,12 +99,13 @@ fun SelectPlatform(
Column(modifier = modifier) {
platforms.forEach { platform ->
- PlatformCheckBoxItem(
- platform = platform,
- title = titles[platform.name]!!,
- description = descriptions[platform.name]!!,
- onClickEvent = onClickEvent
- )
+// TODO(): Handle platform setup
+// PlatformCheckBoxItem(
+// platform = platform,
+// title = titles[platform.name]!!,
+// description = descriptions[platform.name]!!,
+// onClickEvent = onClickEvent
+// )
}
}
}
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/PinnedExitUntilCollapsedScrollBehavior.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/PinnedExitUntilCollapsedScrollBehavior.kt
index a5ed88f6..b8bd54a4 100644
--- a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/PinnedExitUntilCollapsedScrollBehavior.kt
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/PinnedExitUntilCollapsedScrollBehavior.kt
@@ -29,13 +29,12 @@ fun pinnedExitUntilCollapsedScrollBehavior(
canScroll: () -> Boolean = { true },
snapAnimationSpec: AnimationSpec? = spring(stiffness = Spring.StiffnessMediumLow),
flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay()
-): TopAppBarScrollBehavior =
- PinnedExitUntilCollapsedScrollBehavior(
- state = state,
- snapAnimationSpec = snapAnimationSpec,
- flingAnimationSpec = flingAnimationSpec,
- canScroll = canScroll
- )
+): TopAppBarScrollBehavior = PinnedExitUntilCollapsedScrollBehavior(
+ state = state,
+ snapAnimationSpec = snapAnimationSpec,
+ flingAnimationSpec = flingAnimationSpec,
+ canScroll = canScroll
+)
@OptIn(ExperimentalMaterial3Api::class)
private class PinnedExitUntilCollapsedScrollBehavior(
diff --git a/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/PlatformName.kt b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/PlatformName.kt
new file mode 100644
index 00000000..7f08317b
--- /dev/null
+++ b/app/src/main/kotlin/dev/chungjungsoo/gptmobile/util/PlatformName.kt
@@ -0,0 +1,5 @@
+package dev.chungjungsoo.gptmobile.util
+
+import dev.chungjungsoo.gptmobile.data.database.entity.PlatformV2
+
+fun List.getPlatformName(uid: String): String = this.find { it.uid == uid }?.name ?: "Unknown"
diff --git a/app/src/main/res/drawable/ic_gpt_mobile_no_padding.xml b/app/src/main/res/drawable/ic_gpt_mobile_no_padding.xml
new file mode 100644
index 00000000..ee830c54
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gpt_mobile_no_padding.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_select.xml b/app/src/main/res/drawable/ic_select.xml
new file mode 100644
index 00000000..05949148
--- /dev/null
+++ b/app/src/main/res/drawable/ic_select.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..65153c3a
--- /dev/null
+++ b/app/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+
+
+ GPTMobile
+ GPT Mobile Einführung Logo
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 20033c49..73386a72 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -191,4 +191,16 @@
Edit User Message
User Message
Export Chat
+ Migration Assistant
+ The migration assistant will guide you to smoothly migrate to the upgraded database and data structures for the renewed version. Please follow the steps below.
+ Migrate Platforms
+ Migrate
+ Migrate Chats
+ Enabled platforms: %1s
+ Existing chats: %1s
+ Migrating
+ Migrated
+ Error
+ Select Text
+ Unknown
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1056f764..ca745563 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,23 +1,22 @@
[versions]
-agp = "8.7.3"
+agp = "8.8.2"
autoLicense = "11.2.2"
-kotlin = "2.0.20"
+kotlin = "2.0.21"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntime = "2.8.7"
-activityCompose = "1.9.3"
-composeBom = "2024.11.00"
-datastore = "1.1.1"
-gemini = "0.9.0"
-hilt = "2.52"
-ksp = "2.0.20-1.0.25" # Also change with Kotlin version
-ktor = "2.3.12"
+activityCompose = "1.10.1"
+composeBom = "2025.02.00"
+datastore = "1.1.3"
+hilt = "2.55"
+ksp = "2.0.21-1.0.28" # Also change with Kotlin version
+ktor = "3.0.3"
androidxHilt = "1.2.0"
-navigation = "2.8.4"
-markdown = "0.5.4"
-openai = "3.8.2"
+navigation = "2.8.8"
+markdown = "1.0.0-alpha02"
+openai = "4.0.1"
serialization = "1.7.3" # Should not update due to kotlin version
splashscreen = "1.0.1"
room = "2.6.1"
@@ -42,8 +41,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
auto-license-core = { group = "com.mikepenz", name = "aboutlibraries-core", version.ref = "autoLicense" }
auto-license-ui = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "autoLicense" }
-compose-markdown = { group = "com.github.jeziellago", name = "compose-markdown", version.ref = "markdown" }
-gemini = { group = "com.google.ai.client.generativeai", name = "generativeai", version.ref = "gemini"}
+compose-markdown = { group = "com.halilibo.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" }
hilt = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHilt" }
@@ -55,6 +53,7 @@ ktor-logging = { group = "io.ktor", name = "ktor-client-logging-jvm", version.re
ktor-serialization = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
openai = { group = "com.aallam.openai", name = "openai-client", version.ref = "openai" }
splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }
+richtext = { group = "com.halilibo.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown"}
room = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index acc38ffc..8f60d4f9 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Mon May 20 13:06:02 KST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists