Initial commit
This commit is contained in:
commit
cda6eb7ce0
680 changed files with 75081 additions and 0 deletions
25
app/src/main/AndroidManifest.xml
Normal file
25
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MistralChat">
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
141
app/src/main/java/com/mistral/chat/api/MistralClient.kt
Normal file
141
app/src/main/java/com/mistral/chat/api/MistralClient.kt
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package com.mistral.chat.api
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import com.mistral.chat.data.Message
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MistralClient(private val apiKey: String) {
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private val gson = Gson()
|
||||
private val jsonMediaType = "application/json".toMediaType()
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://api.mistral.ai/v1"
|
||||
|
||||
val AVAILABLE_MODELS = listOf(
|
||||
"mistral-small-latest" to "Mistral Small",
|
||||
"mistral-medium-latest" to "Mistral Medium",
|
||||
"mistral-large-latest" to "Mistral Large",
|
||||
"codestral-latest" to "Codestral",
|
||||
"pixtral-large-latest" to "Pixtral Large"
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getModels(): Result<List<Pair<String, String>>> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/models")
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
return@withContext Result.failure(Exception("API error: ${response.code}"))
|
||||
}
|
||||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val responseJson = gson.fromJson(responseBody, JsonObject::class.java)
|
||||
|
||||
val models = responseJson
|
||||
.getAsJsonArray("data")
|
||||
?.mapNotNull { obj ->
|
||||
val jsonObj = obj.asJsonObject
|
||||
val id = jsonObj.get("id")?.asString
|
||||
val created = jsonObj.get("created")?.asLong ?: 0L
|
||||
if (id != null && created > 0 && id.endsWith("-latest")) {
|
||||
val displayName = id
|
||||
.replace("-latest", "")
|
||||
.replace("-", " ")
|
||||
.split(" ")
|
||||
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
|
||||
id to displayName
|
||||
} else null
|
||||
} ?: emptyList()
|
||||
|
||||
Result.success(models)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun chat(
|
||||
model: String,
|
||||
messages: List<Message>,
|
||||
onChunk: ((String) -> Unit)? = null
|
||||
): Result<Pair<String, String>> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val jsonObject = JsonObject()
|
||||
jsonObject.addProperty("model", model)
|
||||
jsonObject.addProperty("temperature", 0.7)
|
||||
jsonObject.addProperty("stream", onChunk != null)
|
||||
|
||||
val messagesArray = JsonArray()
|
||||
messages.forEach { msg ->
|
||||
val msgObj = JsonObject()
|
||||
msgObj.addProperty("role", if (msg.isUser) "user" else "assistant")
|
||||
msgObj.addProperty("content", msg.content)
|
||||
messagesArray.add(msgObj)
|
||||
}
|
||||
jsonObject.add("messages", messagesArray)
|
||||
|
||||
val json = gson.toJson(jsonObject)
|
||||
val body = json.toRequestBody(jsonMediaType)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/chat/completions")
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
val errorBody = response.body?.string() ?: "Unknown error"
|
||||
return@withContext Result.failure(Exception("API error: ${response.code} - $errorBody"))
|
||||
}
|
||||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
||||
if (onChunk != null) {
|
||||
onChunk(responseBody)
|
||||
}
|
||||
|
||||
val responseJson = gson.fromJson(responseBody, JsonObject::class.java)
|
||||
|
||||
val choices = responseJson.getAsJsonArray("choices")
|
||||
if (choices == null || choices.size() == 0) {
|
||||
return@withContext Result.failure(Exception("No response from API"))
|
||||
}
|
||||
|
||||
val content = choices
|
||||
.get(0)
|
||||
?.asJsonObject
|
||||
?.getAsJsonObject("message")
|
||||
?.get("content")
|
||||
?.asString ?: ""
|
||||
|
||||
val usedModel = responseJson.get("model")?.asString ?: model
|
||||
|
||||
Result.success(content to usedModel)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
app/src/main/java/com/mistral/chat/data/Message.kt
Normal file
27
app/src/main/java/com/mistral/chat/data/Message.kt
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
data class Message(
|
||||
val id: String = System.currentTimeMillis().toString(),
|
||||
val content: String,
|
||||
val isUser: Boolean,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val senderName: String? = null
|
||||
)
|
||||
|
||||
data class ChatRequest(
|
||||
val model: String,
|
||||
val messages: List<Message>,
|
||||
val temperature: Double = 0.7,
|
||||
val stream: Boolean = false
|
||||
)
|
||||
|
||||
data class ChatResponse(
|
||||
val id: String,
|
||||
val choices: List<Choice>,
|
||||
val model: String
|
||||
)
|
||||
|
||||
data class Choice(
|
||||
val index: Int,
|
||||
val message: Message
|
||||
)
|
||||
17
app/src/main/java/com/mistral/chat/data/UserProfile.kt
Normal file
17
app/src/main/java/com/mistral/chat/data/UserProfile.kt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package com.mistral.chat.data
|
||||
|
||||
data class UserProfile(
|
||||
val name: String = "",
|
||||
val bio: String = "",
|
||||
val preferences: String = ""
|
||||
) {
|
||||
fun isEmpty(): Boolean = name.isBlank() && bio.isBlank() && preferences.isBlank()
|
||||
|
||||
fun toContextString(): String {
|
||||
return buildString {
|
||||
if (name.isNotBlank()) append("Name: $name\n")
|
||||
if (bio.isNotBlank()) append("Bio: $bio\n")
|
||||
if (preferences.isNotBlank()) append("Preferences: $preferences\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
272
app/src/main/java/com/mistral/chat/ui/MainActivity.kt
Normal file
272
app/src/main/java/com/mistral/chat/ui/MainActivity.kt
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
package com.mistral.chat.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.mistral.chat.R
|
||||
import com.mistral.chat.api.MistralClient
|
||||
import com.mistral.chat.data.Message
|
||||
import com.mistral.chat.data.UserProfile
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var adapter: MessageAdapter
|
||||
private lateinit var inputField: TextInputEditText
|
||||
private lateinit var sendButton: MaterialButton
|
||||
private lateinit var modelSelector: AutoCompleteTextView
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var progressIndicator: LinearProgressIndicator
|
||||
|
||||
private var client: MistralClient? = null
|
||||
private val messages = mutableListOf<Message>()
|
||||
private var availableModels: List<Pair<String, String>> = emptyList()
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
companion object {
|
||||
private const val API_KEY = "YW0IjDBRLuyEBcgNjVeVUFlMI6fcZYLA"
|
||||
private const val PREFS_NAME = "mistral_chat_prefs"
|
||||
private const val KEY_USER_NAME = "user_name"
|
||||
private const val KEY_USER_BIO = "user_bio"
|
||||
private const val KEY_USER_PREFS = "user_preferences"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
client = MistralClient(API_KEY)
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
recyclerView = findViewById(R.id.recyclerView)
|
||||
inputField = findViewById(R.id.inputField)
|
||||
sendButton = findViewById(R.id.sendButton)
|
||||
modelSelector = findViewById(R.id.modelSelector)
|
||||
progressIndicator = findViewById(R.id.progressIndicator)
|
||||
|
||||
setupToolbar()
|
||||
setupRecyclerView()
|
||||
loadModels()
|
||||
setupInput()
|
||||
|
||||
inputField.requestFocus()
|
||||
inputField.postDelayed({
|
||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(inputField, InputMethodManager.SHOW_IMPLICIT)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
toolbar.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_profile -> {
|
||||
showProfileDialog()
|
||||
true
|
||||
}
|
||||
R.id.action_clear -> {
|
||||
showClearChatDialog()
|
||||
true
|
||||
}
|
||||
R.id.action_about -> {
|
||||
showAboutDialog()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
adapter = MessageAdapter(messages)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this).apply {
|
||||
stackFromEnd = true
|
||||
}
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun setupInput() {
|
||||
sendButton.setOnClickListener {
|
||||
val userInput = inputField.text?.toString()?.trim()
|
||||
if (userInput.isNullOrEmpty()) return@setOnClickListener
|
||||
|
||||
addMessage(Message(content = userInput, isUser = true))
|
||||
inputField.text?.clear()
|
||||
|
||||
sendMessage(userInput)
|
||||
}
|
||||
|
||||
inputField.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
toolbar.title = "Mistral Chat"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadModels() {
|
||||
lifecycleScope.launch {
|
||||
val result = client?.getModels()
|
||||
result?.onSuccess { models ->
|
||||
availableModels = models
|
||||
runOnUiThread {
|
||||
val modelNames = models.map { it.second }
|
||||
val adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_dropdown_item_1line, modelNames)
|
||||
modelSelector.setAdapter(adapter)
|
||||
|
||||
val codestralIndex = models.indexOfFirst { it.first.contains("codestral", ignoreCase = true) }
|
||||
if (codestralIndex >= 0) {
|
||||
modelSelector.setText(modelNames[codestralIndex], false)
|
||||
} else if (modelNames.isNotEmpty()) {
|
||||
modelSelector.setText(modelNames[0], false)
|
||||
}
|
||||
}
|
||||
}?.onFailure {
|
||||
availableModels = MistralClient.AVAILABLE_MODELS
|
||||
runOnUiThread {
|
||||
val modelNames = availableModels.map { it.second }
|
||||
val adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_dropdown_item_1line, modelNames)
|
||||
modelSelector.setAdapter(adapter)
|
||||
|
||||
val codestralIndex = availableModels.indexOfFirst { it.first.contains("codestral", ignoreCase = true) }
|
||||
if (codestralIndex >= 0) {
|
||||
modelSelector.setText(modelNames[codestralIndex], false)
|
||||
} else {
|
||||
modelSelector.setText(modelNames[0], false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMessage(message: Message) {
|
||||
messages.add(message)
|
||||
adapter.notifyItemInserted(messages.size - 1)
|
||||
recyclerView.scrollToPosition(messages.size - 1)
|
||||
}
|
||||
|
||||
private fun sendMessage(userInput: String) {
|
||||
val selectedModelName = modelSelector.text.toString()
|
||||
|
||||
val matchedModel = availableModels.find { it.second == selectedModelName }
|
||||
val selectedModel = matchedModel?.first ?: "mistral-small-latest"
|
||||
|
||||
val userProfile = loadUserProfile()
|
||||
val profileContext = if (!userProfile.isEmpty()) {
|
||||
"\n[User Profile]\n${userProfile.toContextString()}\n"
|
||||
} else ""
|
||||
|
||||
val apiMessages = messages.map { msg ->
|
||||
Message(
|
||||
content = msg.content,
|
||||
isUser = msg.isUser
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
if (profileContext.isNotEmpty() && apiMessages.isEmpty()) {
|
||||
apiMessages.add(0, Message(content = profileContext, isUser = true))
|
||||
}
|
||||
|
||||
sendButton.isEnabled = false
|
||||
progressIndicator.isVisible = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
val result = client?.chat(selectedModel, apiMessages)
|
||||
|
||||
result?.onSuccess { (response, usedModel) ->
|
||||
addMessage(Message(content = response, isUser = false, senderName = usedModel))
|
||||
}?.onFailure { error ->
|
||||
addMessage(Message(content = "Error: ${error.message}", isUser = false, senderName = "Error"))
|
||||
}
|
||||
|
||||
sendButton.isEnabled = true
|
||||
progressIndicator.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUserProfile(): UserProfile {
|
||||
return UserProfile(
|
||||
name = prefs.getString(KEY_USER_NAME, "") ?: "",
|
||||
bio = prefs.getString(KEY_USER_BIO, "") ?: "",
|
||||
preferences = prefs.getString(KEY_USER_PREFS, "") ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveUserProfile(profile: UserProfile) {
|
||||
prefs.edit()
|
||||
.putString(KEY_USER_NAME, profile.name)
|
||||
.putString(KEY_USER_BIO, profile.bio)
|
||||
.putString(KEY_USER_PREFS, profile.preferences)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun deleteUserProfile() {
|
||||
prefs.edit()
|
||||
.remove(KEY_USER_NAME)
|
||||
.remove(KEY_USER_BIO)
|
||||
.remove(KEY_USER_PREFS)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun showProfileDialog() {
|
||||
val profile = loadUserProfile()
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_profile, null)
|
||||
|
||||
val nameInput = dialogView.findViewById<TextInputEditText>(R.id.nameInput)
|
||||
val bioInput = dialogView.findViewById<TextInputEditText>(R.id.bioInput)
|
||||
val preferencesInput = dialogView.findViewById<TextInputEditText>(R.id.preferencesInput)
|
||||
|
||||
nameInput.setText(profile.name)
|
||||
bioInput.setText(profile.bio)
|
||||
preferencesInput.setText(profile.preferences)
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.save) { _, _ ->
|
||||
val newProfile = UserProfile(
|
||||
name = nameInput.text?.toString() ?: "",
|
||||
bio = bioInput.text?.toString() ?: "",
|
||||
preferences = preferencesInput.text?.toString() ?: ""
|
||||
)
|
||||
saveUserProfile(newProfile)
|
||||
toolbar.subtitle = if (newProfile.name.isNotBlank()) newProfile.name else null
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setNeutralButton(R.string.delete) { _, _ ->
|
||||
deleteUserProfile()
|
||||
toolbar.subtitle = null
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showClearChatDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.clear_chat_confirm)
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
messages.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showAboutDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.about_text)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
80
app/src/main/java/com/mistral/chat/ui/MessageAdapter.kt
Normal file
80
app/src/main/java/com/mistral/chat/ui/MessageAdapter.kt
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package com.mistral.chat.ui
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mistral.chat.R
|
||||
import com.mistral.chat.data.Message
|
||||
|
||||
class MessageAdapter(private val messages: List<Message>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (messages[position].isUser) 0 else 1
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if (viewType == 0) {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_message_user, parent, false)
|
||||
UserMessageHolder(view)
|
||||
} else {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_message_assistant, parent, false)
|
||||
AssistantMessageHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val message = messages[position]
|
||||
when (holder) {
|
||||
is UserMessageHolder -> holder.bind(message)
|
||||
is AssistantMessageHolder -> holder.bind(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = messages.size
|
||||
|
||||
class UserMessageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
fun bind(message: Message) {
|
||||
val textView = itemView.findViewById<TextView>(R.id.messageText)
|
||||
textView.text = message.content
|
||||
textView.setBackgroundResource(R.drawable.bg_message_user)
|
||||
textView.setTextColor(0xFFFFFFFF.toInt())
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
copyToClipboard(itemView.context, message.content)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AssistantMessageHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
fun bind(message: Message) {
|
||||
val senderNameView = itemView.findViewById<TextView>(R.id.senderName)
|
||||
val textView = itemView.findViewById<TextView>(R.id.messageText)
|
||||
|
||||
senderNameView.text = message.senderName ?: "Assistant"
|
||||
textView.text = message.content
|
||||
textView.setBackgroundResource(R.drawable.bg_message_assistant)
|
||||
textView.setTextColor(0xFF000000.toInt())
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
copyToClipboard(itemView.context, message.content)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun copyToClipboard(context: Context, text: String) {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Message", text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
7
app/src/main/res/drawable/bg_message_assistant.xml
Normal file
7
app/src/main/res/drawable/bg_message_assistant.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#E0E0E0"/>
|
||||
<corners android:radius="16dp"/>
|
||||
<padding android:left="12dp" android:top="8dp" android:right="12dp" android:bottom="8dp"/>
|
||||
</shape>
|
||||
7
app/src/main/res/drawable/bg_message_user.xml
Normal file
7
app/src/main/res/drawable/bg_message_user.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#1976D2"/>
|
||||
<corners android:radius="16dp"/>
|
||||
<padding android:left="12dp" android:top="8dp" android:right="12dp" android:bottom="8dp"/>
|
||||
</shape>
|
||||
17
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
17
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,35 L54,73 M35,54 L73,54"
|
||||
android:strokeWidth="6"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,42 A12,12 0 1,1 54,66 A12,12 0 1,1 54,42"
|
||||
android:strokeWidth="3"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
117
app/src/main/res/layout/activity_main.xml
Normal file
117
app/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:title="Mistral Chat"
|
||||
app:titleCentered="true"
|
||||
app:menu="@menu/main_menu" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/modelSelectorLayout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/select_model"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/modelSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toTopOf="@id/inputCard"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/modelSelectorLayout" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/inputCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="28dp"
|
||||
app:cardElevation="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/inputField"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="@string/enter_message"
|
||||
android:imeOptions="actionSend"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="5"
|
||||
android:minHeight="56dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/sendButton"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:icon="@android:drawable/ic_menu_send"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progressIndicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_anchor="@id/appBarLayout"
|
||||
app:layout_anchorGravity="bottom" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
68
app/src/main/res/layout/dialog_profile.xml
Normal file
68
app/src/main/res/layout/dialog_profile.xml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/profile_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadlineSmall"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/nameLayout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/profile_name">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/nameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/bioLayout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/profile_bio">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/bioInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="3"
|
||||
android:maxLines="5" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/preferencesLayout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/profile_preferences">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/preferencesInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2"
|
||||
android:maxLines="4" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
29
app/src/main/res/layout/item_message_assistant.xml
Normal file
29
app/src/main/res/layout/item_message_assistant.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/senderName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#888888"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="280dp"
|
||||
android:padding="12dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/senderName" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
19
app/src/main/res/layout/item_message_user.xml
Normal file
19
app/src/main/res/layout/item_message_user.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="280dp"
|
||||
android:padding="12dp"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
22
app/src/main/res/menu/main_menu.xml
Normal file
22
app/src/main/res/menu/main_menu.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_profile"
|
||||
android:icon="@android:drawable/ic_menu_myplaces"
|
||||
android:title="@string/profile"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_clear"
|
||||
android:icon="@android:drawable/ic_menu_delete"
|
||||
android:title="@string/clear_chat"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_about"
|
||||
android:title="@string/about"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-hdpi/ic_launcher.xml
Normal file
6
app/src/main/res/mipmap-hdpi/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#6B4EFF"/>
|
||||
<size android:width="48dp" android:height="48dp"/>
|
||||
</shape>
|
||||
4
app/src/main/res/values/colors.xml
Normal file
4
app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#6B4EFF</color>
|
||||
</resources>
|
||||
27
app/src/main/res/values/strings.xml
Normal file
27
app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Mistral Chat</string>
|
||||
<string name="select_model">Select Model</string>
|
||||
<string name="enter_message">Enter message</string>
|
||||
<string name="send">Send</string>
|
||||
<string name="profile">Profile</string>
|
||||
<string name="clear_chat">Clear Chat</string>
|
||||
<string name="about">About</string>
|
||||
<string name="profile_title">User Profile</string>
|
||||
<string name="profile_name">Name</string>
|
||||
<string name="profile_bio">Bio</string>
|
||||
<string name="profile_preferences">Preferences</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="clear_chat_confirm">Clear all messages?</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="chat_cleared">Chat cleared</string>
|
||||
<string name="profile_saved">Profile saved</string>
|
||||
<string name="profile_deleted">Profile deleted</string>
|
||||
<string name="about_text">Mistral Chat\nPowered by Mistral AI</string>
|
||||
<string name="name_hint">Your name</string>
|
||||
<string name="bio_hint">Tell AI about yourself...</string>
|
||||
<string name="no_profile">No profile set</string>
|
||||
</resources>
|
||||
21
app/src/main/res/values/themes.xml
Normal file
21
app/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.MistralChat" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">#6750A4</item>
|
||||
<item name="colorOnPrimary">#FFFFFF</item>
|
||||
<item name="colorPrimaryContainer">#EADDFF</item>
|
||||
<item name="colorOnPrimaryContainer">#21005D</item>
|
||||
<item name="colorSecondary">#625B71</item>
|
||||
<item name="colorOnSecondary">#FFFFFF</item>
|
||||
<item name="colorSecondaryContainer">#E8DEF8</item>
|
||||
<item name="colorOnSecondaryContainer">#1D192B</item>
|
||||
<item name="colorTertiary">#7D5260</item>
|
||||
<item name="colorOnTertiary">#FFFFFF</item>
|
||||
<item name="colorTertiaryContainer">#FFD8E4</item>
|
||||
<item name="colorOnTertiaryContainer">#31111D</item>
|
||||
<item name="colorError">#B3261E</item>
|
||||
<item name="colorOnError">#FFFFFF</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue