diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 17fadf4..b661762 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,11 @@
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/Theme.Pixel10AI">
+
+
()
+ private lateinit var adapter: MessageAdapter
+ private var service: ApiServerService? = null
+ private var bound = false
+ private var generating = false
+
+ private val serviceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
+ service = (binder as ApiServerService.LocalBinder).service
+ bound = true
+ }
+ override fun onServiceDisconnected(name: ComponentName?) {
+ service = null
+ bound = false
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityChatBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setSupportActionBar(binding.toolbar)
+ binding.toolbar.setNavigationOnClickListener { finish() }
+
+ adapter = MessageAdapter(messages)
+ binding.rvMessages.layoutManager = LinearLayoutManager(this).also {
+ it.stackFromEnd = true
+ }
+ binding.rvMessages.adapter = adapter
+
+ binding.btnSend.setOnClickListener { sendMessage() }
+ binding.etMessage.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_SEND) { sendMessage(); true } else false
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ bindService(
+ Intent(this, ApiServerService::class.java),
+ serviceConnection,
+ Context.BIND_AUTO_CREATE
+ )
+ }
+
+ override fun onStop() {
+ super.onStop()
+ if (bound) { unbindService(serviceConnection); bound = false }
+ }
+
+ private fun sendMessage() {
+ if (generating) return
+ val text = binding.etMessage.text.toString().trim()
+ if (text.isEmpty()) return
+
+ binding.etMessage.text?.clear()
+
+ // Add user message
+ messages.add(ChatMessage("user", text))
+ adapter.notifyItemInserted(messages.size - 1)
+ scrollToBottom()
+
+ // Add empty AI placeholder
+ messages.add(ChatMessage("assistant", ""))
+ val aiIndex = messages.size - 1
+ adapter.notifyItemInserted(aiIndex)
+ scrollToBottom()
+
+ binding.tvTyping.visibility = View.VISIBLE
+ binding.btnSend.isEnabled = false
+ generating = true
+
+ val model = service?.currentModel
+ if (model == null || !model.isReady) {
+ messages[aiIndex].content = "⚠️ Server not running — start the server first."
+ adapter.notifyItemChanged(aiIndex)
+ finishGeneration()
+ return
+ }
+
+ val prompt = buildPrompt()
+
+ lifecycleScope.launch {
+ try {
+ model.generateStreaming(prompt) { token ->
+ runOnUiThread {
+ messages[aiIndex].content += token
+ adapter.notifyItemChanged(aiIndex)
+ scrollToBottom()
+ }
+ }
+ } catch (e: Exception) {
+ runOnUiThread {
+ messages[aiIndex].content = "⚠️ Error: ${e.message}"
+ adapter.notifyItemChanged(aiIndex)
+ }
+ } finally {
+ runOnUiThread { finishGeneration() }
+ }
+ }
+ }
+
+ private fun finishGeneration() {
+ generating = false
+ binding.tvTyping.visibility = View.GONE
+ binding.btnSend.isEnabled = true
+ scrollToBottom()
+ }
+
+ private fun buildPrompt(): String {
+ val prefs = getSharedPreferences("pixel10_prefs", MODE_PRIVATE)
+ val useSystemPrompt = prefs.getBoolean("auto_system_prompt", true)
+
+ val sb = StringBuilder()
+ if (useSystemPrompt) {
+ sb.append("System: ${AgentConfig.SYSTEM_PROMPT}\n\n")
+ }
+ // Include all messages except the last empty AI placeholder
+ for (i in 0 until messages.size - 1) {
+ val msg = messages[i]
+ when (msg.role) {
+ "user" -> sb.append("User: ${msg.content}\n\n")
+ "assistant" -> sb.append("Assistant: ${msg.content}\n\n")
+ }
+ }
+ sb.append("Assistant:")
+ return sb.toString()
+ }
+
+ private fun scrollToBottom() {
+ if (messages.isNotEmpty()) {
+ binding.rvMessages.smoothScrollToPosition(messages.size - 1)
+ }
+ }
+}
diff --git a/app/src/main/java/com/pixel10/ai/ui/ChatMessage.kt b/app/src/main/java/com/pixel10/ai/ui/ChatMessage.kt
new file mode 100644
index 0000000..af79b8e
--- /dev/null
+++ b/app/src/main/java/com/pixel10/ai/ui/ChatMessage.kt
@@ -0,0 +1,3 @@
+package com.pixel10.ai.ui
+
+data class ChatMessage(val role: String, var content: String)
diff --git a/app/src/main/java/com/pixel10/ai/ui/MainActivity.kt b/app/src/main/java/com/pixel10/ai/ui/MainActivity.kt
index db50e2d..d42284a 100644
--- a/app/src/main/java/com/pixel10/ai/ui/MainActivity.kt
+++ b/app/src/main/java/com/pixel10/ai/ui/MainActivity.kt
@@ -104,6 +104,10 @@ class MainActivity : AppCompatActivity() {
if (service?.isRunning == true) stopServer() else startServer()
}
+ binding.btnChat.setOnClickListener {
+ startActivity(Intent(this, ChatActivity::class.java))
+ }
+
binding.btnDownloadModel.setOnClickListener {
saveHfToken()
startModelDownload(ModelSpec.GEMMA_3N_E4B)
@@ -272,6 +276,7 @@ class MainActivity : AppCompatActivity() {
binding.tvModelStatus.text = "Model: not loaded"
binding.btnToggle.text = getString(R.string.btn_start)
binding.btnToggle.isEnabled = true
+ binding.btnChat.isEnabled = false
}
ApiServerService.ServerState.LOADING_MODEL -> {
binding.tvServerStatus.text = getString(R.string.server_status_starting)
@@ -288,6 +293,7 @@ class MainActivity : AppCompatActivity() {
binding.tvModelStatus.text = getString(R.string.model_ready)
binding.btnToggle.text = getString(R.string.btn_stop)
binding.btnToggle.isEnabled = true
+ binding.btnChat.isEnabled = true
}
ApiServerService.ServerState.ERROR -> {
binding.tvServerStatus.text = getString(R.string.server_status_error)
diff --git a/app/src/main/java/com/pixel10/ai/ui/MessageAdapter.kt b/app/src/main/java/com/pixel10/ai/ui/MessageAdapter.kt
new file mode 100644
index 0000000..0fc8073
--- /dev/null
+++ b/app/src/main/java/com/pixel10/ai/ui/MessageAdapter.kt
@@ -0,0 +1,43 @@
+package com.pixel10.ai.ui
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.pixel10.ai.R
+
+class MessageAdapter(private val messages: List) :
+ RecyclerView.Adapter() {
+
+ companion object {
+ private const val TYPE_USER = 0
+ private const val TYPE_AI = 1
+ }
+
+ override fun getItemViewType(position: Int) =
+ if (messages[position].role == "user") TYPE_USER else TYPE_AI
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ return if (viewType == TYPE_USER) {
+ val view = inflater.inflate(R.layout.item_message_user, parent, false)
+ UserViewHolder(view.findViewById(R.id.tvContent))
+ } else {
+ val view = inflater.inflate(R.layout.item_message_ai, parent, false)
+ AiViewHolder(view.findViewById(R.id.tvContent))
+ }
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val msg = messages[position]
+ when (holder) {
+ is UserViewHolder -> holder.tv.text = msg.content
+ is AiViewHolder -> holder.tv.text = msg.content.ifEmpty { "▌" }
+ }
+ }
+
+ override fun getItemCount() = messages.size
+
+ class UserViewHolder(val tv: TextView) : RecyclerView.ViewHolder(tv.parent as android.view.View)
+ class AiViewHolder(val tv: TextView) : RecyclerView.ViewHolder(tv.parent as android.view.View)
+}
diff --git a/app/src/main/res/drawable/bubble_ai.xml b/app/src/main/res/drawable/bubble_ai.xml
new file mode 100644
index 0000000..e8fa768
--- /dev/null
+++ b/app/src/main/res/drawable/bubble_ai.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bubble_user.xml b/app/src/main/res/drawable/bubble_user.xml
new file mode 100644
index 0000000..35b96b5
--- /dev/null
+++ b/app/src/main/res/drawable/bubble_user.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 0000000..0237090
--- /dev/null
+++ b/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml
new file mode 100644
index 0000000..ce1c496
--- /dev/null
+++ b/app/src/main/res/layout/activity_chat.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index a1d3ed2..2f15536 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -324,18 +324,38 @@
-
-
+
+ app:layout_constraintEnd_toEndOf="parent">
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_message_ai.xml b/app/src/main/res/layout/item_message_ai.xml
new file mode 100644
index 0000000..78e87fb
--- /dev/null
+++ b/app/src/main/res/layout/item_message_ai.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_message_user.xml b/app/src/main/res/layout/item_message_user.xml
new file mode 100644
index 0000000..ef72251
--- /dev/null
+++ b/app/src/main/res/layout/item_message_user.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+