chore: 项目已迁移至 Gitee/GitHub,仅保留迁移说明 (tombstone)

This commit is contained in:
shuiyuPyLab committed 2026-08-09 10:16:20 +08:00
1 parent 125897b87b
commit d4f17a8828
40 files changed
+1 -1915

No files matched your search

-11
View File
@@ -1,11 +0,0 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
/tmp
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 shuiyu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -2
View File
@@ -2,8 +2,7 @@
你好呀~这个仓库已经搬到更好用的地方啦。
为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到
**Gitee** 与 **GitHub** 主仓维护,这里就只保留这张"迁移小纸条"咯。
为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到主仓维护,这里就只保留这张"迁移小纸条"咯。
📦 新家在这里:
- Gitee:https://gitee.com/shuiyu1123/mimo-studio-android
-66
View File
@@ -1,66 +0,0 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21"
}
android {
namespace = "com.mimo.studio"
compileSdk = 35
defaultConfig {
applicationId = "com.mimo.studio"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0.0"
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
// Retrofit + OkHttp
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.squareup.okhttp3:okhttp-sse:4.12.0")
// Gson
implementation("com.google.code.gson:gson:2.11.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}
-5
View File
@@ -1,5 +0,0 @@
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-keep class com.mimo.studio.model.** { *; }
-keep class com.mimo.studio.api.** { *; }
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.MimoStudio"
android:usesCleartextTraffic="false">
<activity
android:name=".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>
<activity
android:name=".ui.ModelsActivity"
android:exported="false"
android:parentActivityName=".MainActivity" />
</application>
</manifest>
@@ -1,266 +0,0 @@
package com.mimo.studio
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationView
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputEditText
import com.mimo.studio.api.RetrofitClient
import com.mimo.studio.model.ChatMessage
import com.mimo.studio.model.ModelInfo
import com.mimo.studio.ui.ChatAdapter
import com.mimo.studio.ui.ModelsActivity
import com.mimo.studio.viewmodel.MainViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var chatAdapter: ChatAdapter
private lateinit var drawerLayout: DrawerLayout
private lateinit var recyclerChat: RecyclerView
private lateinit var etMessage: EditText
private lateinit var btnSend: ImageButton
private lateinit var progressBar: ProgressBar
private lateinit var tvStreaming: TextView
private lateinit var spinnerModel: Spinner
private var modelNames = listOf("xiaomi/mimo-v2-flash", "xiaomi/mimo-v2-pro", "xiaomi/mimo-v2-omni", "xiaomi/mimo-v2.5-pro", "xiaomi/mimo-v2.5")
private var modelDisplayNames = listOf("MiMo-V2-Flash", "MiMo-V2-Pro", "MiMo-V2-Omni", "MiMo-V2.5-Pro", "MiMo-V2.5")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = MainViewModel(application)
initViews()
setupDrawer()
setupChat()
setupModelSpinner()
observeState()
// Check if API key is configured
if (viewModel.getApiKey().isBlank()) {
showSettingsDialog()
} else {
viewModel.fetchModels()
}
}
private fun initViews() {
drawerLayout = findViewById(R.id.drawerLayout)
recyclerChat = findViewById(R.id.recyclerChat)
etMessage = findViewById(R.id.etMessage)
btnSend = findViewById(R.id.btnSend)
progressBar = findViewById(R.id.progressBar)
tvStreaming = findViewById(R.id.tvStreaming)
spinnerModel = findViewById(R.id.spinnerModel)
}
private fun setupDrawer() {
val toolbar = findViewById<MaterialToolbar>(R.id.toolbar)
val toggle = ActionBarDrawerToggle(
this, drawerLayout, toolbar,
R.string.app_name, R.string.app_name
)
drawerLayout.addDrawerListener(toggle)
toggle.syncState()
val navView = findViewById<NavigationView>(R.id.navView)
navView.setNavigationItemSelectedListener { item ->
drawerLayout.closeDrawer(GravityCompat.START)
when (item.itemId) {
R.id.nav_new_chat -> viewModel.newConversation()
R.id.nav_models -> {
startActivity(Intent(this, ModelsActivity::class.java))
}
R.id.nav_settings -> showSettingsDialog()
}
true
}
}
private fun setupChat() {
chatAdapter = ChatAdapter()
recyclerChat.layoutManager = LinearLayoutManager(this).apply {
stackFromEnd = true
}
recyclerChat.adapter = chatAdapter
btnSend.setOnClickListener {
val text = etMessage.text.toString().trim()
if (text.isNotEmpty()) {
viewModel.sendMessage(text)
etMessage.text.clear()
}
}
}
private fun setupModelSpinner() {
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, modelDisplayNames)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerModel.adapter = adapter
spinnerModel.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
viewModel.setSelectedModel(modelNames[pos])
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun observeState() {
lifecycleScope.launch {
viewModel.currentConversation.collectLatest { conv ->
if (conv != null) {
chatAdapter.submitList(conv.messages.toList())
if (conv.messages.isNotEmpty()) {
recyclerChat.post {
recyclerChat.smoothScrollToPosition(conv.messages.size - 1)
}
}
}
}
}
lifecycleScope.launch {
viewModel.isLoading.collectLatest { loading ->
progressBar.visibility = if (loading) View.VISIBLE else View.GONE
btnSend.isEnabled = !loading
}
}
lifecycleScope.launch {
viewModel.isStreaming.collectLatest { streaming ->
tvStreaming.visibility = if (streaming) View.VISIBLE else View.GONE
}
}
lifecycleScope.launch {
viewModel.streamingContent.collectLatest { content ->
if (content.isNotEmpty()) {
tvStreaming.text = content
chatAdapter.updateStreamingContent(content)
recyclerChat.post {
recyclerChat.smoothScrollToPosition(chatAdapter.itemCount - 1)
}
}
}
}
lifecycleScope.launch {
viewModel.error.collectLatest { error ->
error?.let {
Toast.makeText(this@MainActivity, it, Toast.LENGTH_LONG).show()
viewModel.clearError()
}
}
}
lifecycleScope.launch {
viewModel.selectedModel.collectLatest { model ->
val idx = modelNames.indexOf(model)
if (idx >= 0 && spinnerModel.selectedItemPosition != idx) {
spinnerModel.setSelection(idx)
}
}
}
lifecycleScope.launch {
viewModel.models.collectLatest { models ->
if (models.isNotEmpty()) {
updateModelList(models)
}
}
}
}
private fun updateModelList(models: List<ModelInfo>) {
modelNames = models.map { it.id }
modelDisplayNames = models.map { it.name.removePrefix("Xiaomi MiMo:").removePrefix("Xiaomi MiMo:") }
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, modelDisplayNames)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerModel.adapter = adapter
val idx = modelNames.indexOf(viewModel.selectedModel.value)
if (idx >= 0) spinnerModel.setSelection(idx)
}
private fun showSettingsDialog() {
val dialogView = layoutInflater.inflate(R.layout.dialog_settings, null)
val etApiKey = dialogView.findViewById<TextInputEditText>(R.id.etApiKey)
val etBaseUrl = dialogView.findViewById<TextInputEditText>(R.id.etBaseUrl)
val etSystemPrompt = dialogView.findViewById<TextInputEditText>(R.id.etSystemPrompt)
val seekTemp = dialogView.findViewById<SeekBar>(R.id.seekTemperature)
val tvTemp = dialogView.findViewById<TextView>(R.id.tvTemperature)
val etMaxTokens = dialogView.findViewById<TextInputEditText>(R.id.etMaxTokens)
val switchStreaming = dialogView.findViewById<SwitchMaterial>(R.id.switchStreaming)
etApiKey.setText(viewModel.getApiKey())
etBaseUrl.setText(viewModel.getBaseUrl())
etSystemPrompt.setText(viewModel.currentConversation.value?.systemPrompt ?: "")
seekTemp.progress = (viewModel.temperature.value * 100).toInt()
tvTemp.text = String.format("%.2f", viewModel.temperature.value)
etMaxTokens.setText(viewModel.maxTokens.value.toString())
switchStreaming.isChecked = viewModel.useStreaming.value
seekTemp.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
tvTemp.text = String.format("%.2f", progress / 100f)
}
override fun onStartTrackingTouch(sb: SeekBar?) {}
override fun onStopTrackingTouch(sb: SeekBar?) {}
})
MaterialAlertDialogBuilder(this)
.setTitle("设置")
.setView(dialogView)
.setPositiveButton("保存") { _, _ ->
val apiKey = etApiKey.text.toString().trim()
val baseUrl = etBaseUrl.text.toString().trim().ifEmpty { "https://api.xiaomimimo.com/" }
val systemPrompt = etSystemPrompt.text.toString().trim()
val temp = seekTemp.progress / 100f
val maxTokens = etMaxTokens.text.toString().trim().toIntOrNull() ?: 4096
viewModel.saveSettings(apiKey, baseUrl)
viewModel.setTemperature(temp)
viewModel.setMaxTokens(maxTokens)
viewModel.setStreaming(switchStreaming.isChecked)
// Update system prompt on current conversation
viewModel.currentConversation.value?.let { conv ->
val updated = conv.copy(systemPrompt = systemPrompt)
// Update through viewmodel would be cleaner, but this works
}
if (apiKey.isNotBlank()) {
viewModel.fetchModels()
}
}
.setNegativeButton("取消", null)
.show()
}
@Deprecated("Use OnBackPressedCallback")
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
}
@@ -1,34 +0,0 @@
package com.mimo.studio.api
import com.mimo.studio.model.*
import okhttp3.ResponseBody
import retrofit2.http.*
interface MimoApiService {
/** List all available models */
@GET("v1/models")
suspend fun listModels(): ModelsResponse
/** Get a specific model */
@GET("v1/models/{model_id}")
suspend fun getModel(@Path("model_id") modelId: String): ModelInfo
/** Create a chat completion */
@POST("v1/chat/completions")
suspend fun chatCompletions(@Body request: ChatRequest): ChatResponse
/** Create a streaming chat completion */
@POST("v1/chat/completions")
@Streaming
suspend fun chatCompletionsStream(@Body request: ChatRequest): ResponseBody
/** Create a text completion */
@POST("v1/completions")
suspend fun completions(@Body request: CompletionRequest): CompletionResponse
/** Create a streaming text completion */
@POST("v1/completions")
@Streaming
suspend fun completionsStream(@Body request: CompletionRequest): ResponseBody
}
@@ -1,60 +0,0 @@
package com.mimo.studio.api
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object RetrofitClient {
private const val BASE_URL = "https://api.xiaomimimo.com/"
private var apiKey: String = ""
private var baseUrl: String = BASE_URL
fun configure(apiKey: String, baseUrl: String = BASE_URL) {
this.apiKey = apiKey
this.baseUrl = baseUrl
}
fun isConfigured(): Boolean = apiKey.isNotBlank()
private val authInterceptor = Interceptor { chain ->
val original = chain.request()
val request = original.newBuilder()
.header("Authorization", "Bearer $apiKey")
.header("Content-Type", "application/json")
.build()
chain.proceed(request)
}
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(loggingInterceptor)
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build()
private var retrofit: Retrofit? = null
private var currentBaseUrl: String = ""
val api: MimoApiService
get() {
if (retrofit == null || currentBaseUrl != baseUrl) {
retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
currentBaseUrl = baseUrl
}
return retrofit!!.create(MimoApiService::class.java)
}
}
@@ -1,122 +0,0 @@
package com.mimo.studio.model
import com.google.gson.annotations.SerializedName
// ===== Models API =====
data class ModelsResponse(
val data: List<ModelInfo>
)
data class ModelInfo(
val id: String,
val name: String,
val description: String,
@SerializedName("context_length") val contextLength: Int,
@SerializedName("max_output_length") val maxOutputLength: Int,
val architecture: Architecture?,
val pricing: Any?, // Can be object or array
@SerializedName("supported_parameters") val supportedParameters: List<String>?
)
data class Architecture(
val modality: String,
@SerializedName("input_modalities") val inputModalities: List<String>,
@SerializedName("output_modalities") val outputModalities: List<String>
)
// ===== Chat Completions API =====
data class ChatRequest(
val model: String,
val messages: List<ChatMessage>,
val stream: Boolean = false,
@SerializedName("max_tokens") val maxTokens: Int? = null,
val temperature: Float? = null,
@SerializedName("top_p") val topP: Float? = null,
val stop: List<String>? = null,
@SerializedName("frequency_penalty") val frequencyPenalty: Float? = null,
@SerializedName("presence_penalty") val presencePenalty: Float? = null,
@SerializedName("response_format") val responseFormat: ResponseFormat? = null,
val reasoning: Boolean? = null,
@SerializedName("include_reasoning") val includeReasoning: Boolean? = null
)
data class ResponseFormat(
val type: String // "text" or "json_object"
)
data class ChatMessage(
val role: String, // "system", "user", "assistant"
val content: String
)
data class ChatResponse(
val id: String?,
val choices: List<Choice>?,
val usage: Usage?,
val model: String?
)
data class Choice(
val index: Int,
val message: ChatMessage?,
@SerializedName("finish_reason") val finishReason: String?,
val delta: ChatMessage? // for streaming
)
data class Usage(
@SerializedName("prompt_tokens") val promptTokens: Int,
@SerializedName("completion_tokens") val completionTokens: Int,
@SerializedName("total_tokens") val totalTokens: Int
)
// ===== Streaming =====
data class StreamChunk(
val id: String?,
val choices: List<StreamChoice>?,
val model: String?
)
data class StreamChoice(
val index: Int,
val delta: DeltaMessage?,
@SerializedName("finish_reason") val finishReason: String?
)
data class DeltaMessage(
val role: String?,
val content: String?
)
// ===== Completions API =====
data class CompletionRequest(
val model: String,
val prompt: String,
val stream: Boolean = false,
@SerializedName("max_tokens") val maxTokens: Int? = null,
val temperature: Float? = null,
@SerializedName("top_p") val topP: Float? = null,
val stop: List<String>? = null
)
data class CompletionResponse(
val id: String?,
val choices: List<CompletionChoice>?,
val usage: Usage?,
val model: String?
)
data class CompletionChoice(
val index: Int,
val text: String?,
@SerializedName("finish_reason") val finishReason: String?
)
// ===== Local chat state =====
data class Conversation(
val id: String = java.util.UUID.randomUUID().toString(),
val title: String = "新对话",
val messages: MutableList<ChatMessage> = mutableListOf(),
val model: String = "xiaomi/mimo-v2-flash",
val systemPrompt: String = "",
val createdAt: Long = System.currentTimeMillis()
)
@@ -1,103 +0,0 @@
package com.mimo.studio.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.mimo.studio.R
import com.mimo.studio.model.ChatMessage
import java.text.SimpleDateFormat
import java.util.*
class ChatAdapter : ListAdapter<ChatMessage, RecyclerView.ViewHolder>(ChatDiffCallback()) {
companion object {
private const val TYPE_USER = 0
private const val TYPE_ASSISTANT = 1
private const val TYPE_SYSTEM = 2
}
private var timestamp = System.currentTimeMillis()
override fun getItemViewType(position: Int): Int {
return when (getItem(position).role) {
"user" -> TYPE_USER
"assistant" -> TYPE_ASSISTANT
else -> TYPE_SYSTEM
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_USER -> UserViewHolder(inflater.inflate(R.layout.item_chat_user, parent, false))
TYPE_ASSISTANT -> AssistantViewHolder(inflater.inflate(R.layout.item_chat_assistant, parent, false))
else -> SystemViewHolder(inflater.inflate(R.layout.item_chat_assistant, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val message = getItem(position)
val timeStr = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(timestamp + position * 1000))
when (holder) {
is UserViewHolder -> holder.bind(message, timeStr)
is AssistantViewHolder -> holder.bind(message, timeStr)
is SystemViewHolder -> holder.bind(message, timeStr)
}
}
fun updateStreamingContent(content: String) {
// Update the last assistant message with streaming content
// or add a temporary one
val currentList = currentList.toMutableList()
if (currentList.isNotEmpty() && currentList.last().role == "assistant" && currentList.last().content == "") {
currentList[currentList.size - 1] = ChatMessage(role = "assistant", content = content)
} else {
currentList.add(ChatMessage(role = "assistant", content = content))
}
submitList(currentList)
}
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvMessage: TextView = itemView.findViewById(R.id.tvMessage)
private val tvTimestamp: TextView = itemView.findViewById(R.id.tvTimestamp)
fun bind(message: ChatMessage, time: String) {
tvMessage.text = message.content
tvTimestamp.text = time
}
}
class AssistantViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvMessage: TextView = itemView.findViewById(R.id.tvMessage)
private val tvTimestamp: TextView = itemView.findViewById(R.id.tvTimestamp)
fun bind(message: ChatMessage, time: String) {
tvMessage.text = message.content
tvTimestamp.text = time
}
}
class SystemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvMessage: TextView = itemView.findViewById(R.id.tvMessage)
private val tvTimestamp: TextView = itemView.findViewById(R.id.tvTimestamp)
fun bind(message: ChatMessage, time: String) {
tvMessage.text = "⚙️ ${message.content}"
tvTimestamp.text = time
}
}
class ChatDiffCallback : DiffUtil.ItemCallback<ChatMessage>() {
override fun areItemsTheSame(oldItem: ChatMessage, newItem: ChatMessage): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: ChatMessage, newItem: ChatMessage): Boolean {
return oldItem.role == newItem.role && oldItem.content == newItem.content
}
}
}
@@ -1,55 +0,0 @@
package com.mimo.studio.ui
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.appbar.MaterialToolbar
import com.mimo.studio.R
import com.mimo.studio.api.RetrofitClient
import com.mimo.studio.model.ModelInfo
import kotlinx.coroutines.launch
class ModelsActivity : AppCompatActivity() {
private lateinit var adapter: ModelsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_models)
val toolbar = findViewById<MaterialToolbar>(R.id.toolbar)
toolbar.setNavigationOnClickListener { finish() }
adapter = ModelsAdapter { model ->
val intent = intent.putExtra("selected_model", model.id)
setResult(RESULT_OK, intent)
Toast.makeText(this, "已选择: ${model.name}", Toast.LENGTH_SHORT).show()
finish()
}
val recycler = findViewById<androidx.recyclerview.widget.RecyclerView>(R.id.recyclerModels)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = adapter
val swipeRefresh = findViewById<androidx.swiperefreshlayout.widget.SwipeRefreshLayout>(R.id.swipeRefresh)
swipeRefresh.setOnRefreshListener { fetchModels() }
fetchModels()
}
private fun fetchModels() {
val swipeRefresh = findViewById<androidx.swiperefreshlayout.widget.SwipeRefreshLayout>(R.id.swipeRefresh)
lifecycleScope.launch {
try {
val response = RetrofitClient.api.listModels()
adapter.submitList(response.data)
} catch (e: Exception) {
Toast.makeText(this@ModelsActivity, "加载失败: ${e.message}", Toast.LENGTH_LONG).show()
} finally {
swipeRefresh.isRefreshing = false
}
}
}
}
@@ -1,59 +0,0 @@
package com.mimo.studio.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.mimo.studio.R
import com.mimo.studio.model.ModelInfo
class ModelsAdapter(
private val onSelect: (ModelInfo) -> Unit
) : RecyclerView.Adapter<ModelsAdapter.ModelViewHolder>() {
private var models: List<ModelInfo> = emptyList()
fun submitList(list: List<ModelInfo>) {
models = list
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ModelViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_model, parent, false)
return ModelViewHolder(view)
}
override fun onBindViewHolder(holder: ModelViewHolder, position: Int) {
holder.bind(models[position])
}
override fun getItemCount() = models.size
inner class ModelViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tvModelName)
private val tvId: TextView = itemView.findViewById(R.id.tvModelId)
private val tvModality: TextView = itemView.findViewById(R.id.tvModality)
private val tvContext: TextView = itemView.findViewById(R.id.tvContext)
private val tvDesc: TextView = itemView.findViewById(R.id.tvDescription)
private val btnSelect: MaterialButton = itemView.findViewById(R.id.btnSelect)
fun bind(model: ModelInfo) {
tvName.text = model.name
tvId.text = model.id
tvModality.text = "模态: ${model.architecture?.modality ?: "text->text"}"
tvContext.text = "上下文: ${formatTokens(model.contextLength)} tokens | 最大输出: ${formatTokens(model.maxOutputLength)} tokens"
tvDesc.text = model.description
btnSelect.setOnClickListener { onSelect(model) }
}
private fun formatTokens(tokens: Int): String {
return when {
tokens >= 1_000_000 -> "${tokens / 1_000_000}M"
tokens >= 1_000 -> "${tokens / 1_000}K"
else -> tokens.toString()
}
}
}
}
@@ -1,303 +0,0 @@
package com.mimo.studio.viewmodel
import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.mimo.studio.api.RetrofitClient
import com.mimo.studio.model.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val prefs = application.getSharedPreferences("mimo_prefs", Context.MODE_PRIVATE)
private val gson = Gson()
// State
private val _models = MutableStateFlow<List<ModelInfo>>(emptyList())
val models: StateFlow<List<ModelInfo>> = _models
private val _conversations = MutableStateFlow<List<Conversation>>(emptyList())
val conversations: StateFlow<List<Conversation>> = _conversations
private val _currentConversation = MutableStateFlow<Conversation?>(null)
val currentConversation: StateFlow<Conversation?> = _currentConversation
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _isStreaming = MutableStateFlow(false)
val isStreaming: StateFlow<Boolean> = _isStreaming
private val _streamingContent = MutableStateFlow("")
val streamingContent: StateFlow<String> = _streamingContent
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error
private val _selectedModel = MutableStateFlow("xiaomi/mimo-v2-flash")
val selectedModel: StateFlow<String> = _selectedModel
private val _useStreaming = MutableStateFlow(true)
val useStreaming: StateFlow<Boolean> = _useStreaming
private val _temperature = MutableStateFlow(1.0f)
val temperature: StateFlow<Float> = _temperature
private val _maxTokens = MutableStateFlow(4096)
val maxTokens: StateFlow<Int> = _maxTokens
init {
loadSettings()
loadConversations()
if (conversations.value.isEmpty()) {
newConversation()
}
}
// ===== Settings =====
fun getApiKey(): String = prefs.getString("api_key", "") ?: ""
fun getBaseUrl(): String = prefs.getString("base_url", "https://api.xiaomimimo.com/") ?: "https://api.xiaomimimo.com/"
fun saveSettings(apiKey: String, baseUrl: String) {
prefs.edit()
.putString("api_key", apiKey)
.putString("base_url", baseUrl)
.apply()
RetrofitClient.configure(apiKey, baseUrl)
}
private fun loadSettings() {
val key = getApiKey()
val url = getBaseUrl()
if (key.isNotBlank()) {
RetrofitClient.configure(key, url)
}
_selectedModel.value = prefs.getString("selected_model", "xiaomi/mimo-v2-flash") ?: "xiaomi/mimo-v2-flash"
_useStreaming.value = prefs.getBoolean("use_streaming", true)
_temperature.value = prefs.getFloat("temperature", 1.0f)
_maxTokens.value = prefs.getInt("max_tokens", 4096)
}
fun setSelectedModel(model: String) {
_selectedModel.value = model
prefs.edit().putString("selected_model", model).apply()
}
fun setStreaming(enabled: Boolean) {
_useStreaming.value = enabled
prefs.edit().putBoolean("use_streaming", enabled).apply()
}
fun setTemperature(temp: Float) {
_temperature.value = temp
prefs.edit().putFloat("temperature", temp).apply()
}
fun setMaxTokens(tokens: Int) {
_maxTokens.value = tokens
prefs.edit().putInt("max_tokens", tokens).apply()
}
// ===== Models =====
fun fetchModels() {
if (!RetrofitClient.isConfigured()) {
_error.value = "请先配置 API Key"
return
}
viewModelScope.launch {
try {
val response = withContext(Dispatchers.IO) { RetrofitClient.api.listModels() }
_models.value = response.data
_error.value = null
} catch (e: Exception) {
_error.value = "获取模型列表失败: ${e.message}"
}
}
}
// ===== Conversations =====
fun newConversation() {
val conv = Conversation(model = _selectedModel.value)
val list = _conversations.value.toMutableList()
list.add(0, conv)
_conversations.value = list
_currentConversation.value = conv
saveConversations()
}
fun selectConversation(id: String) {
_currentConversation.value = _conversations.value.find { it.id == id }
}
fun deleteConversation(id: String) {
val list = _conversations.value.filter { it.id != id }.toMutableList()
_conversations.value = list
if (_currentConversation.value?.id == id) {
_currentConversation.value = list.firstOrNull()
}
saveConversations()
}
private fun saveConversations() {
val json = gson.toJson(_conversations.value)
prefs.edit().putString("conversations", json).apply()
}
private fun loadConversations() {
val json = prefs.getString("conversations", null)
if (json != null) {
val type = object : TypeToken<List<Conversation>>() {}.type
try {
val list: List<Conversation> = gson.fromJson(json, type)
_conversations.value = list
_currentConversation.value = list.firstOrNull()
} catch (_: Exception) {}
}
}
// ===== Chat =====
fun sendMessage(content: String) {
val conv = _currentConversation.value ?: return
if (!RetrofitClient.isConfigured()) {
_error.value = "请先在设置中配置 API Key"
return
}
// Add user message
conv.messages.add(ChatMessage(role = "user", content = content))
if (conv.messages.size == 1 && conv.title == "新对话") {
// Auto-title from first message
val newConv = conv.copy(title = content.take(30) + if (content.length > 30) "..." else "")
updateConversation(newConv)
} else {
_currentConversation.value = conv.copy()
}
if (_useStreaming.value) {
sendStreamingMessage(conv)
} else {
sendNormalMessage(conv)
}
}
private fun sendNormalMessage(conv: Conversation) {
_isLoading.value = true
_error.value = null
viewModelScope.launch {
try {
val messages = buildMessages(conv)
val request = ChatRequest(
model = conv.model,
messages = messages,
temperature = _temperature.value,
maxTokens = _maxTokens.value
)
val response = withContext(Dispatchers.IO) {
RetrofitClient.api.chatCompletions(request)
}
val assistantMsg = response.choices?.firstOrNull()?.message
if (assistantMsg != null) {
conv.messages.add(assistantMsg)
updateConversation(conv.copy())
}
_error.value = null
} catch (e: Exception) {
_error.value = "请求失败: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
private fun sendStreamingMessage(conv: Conversation) {
_isStreaming.value = true
_streamingContent.value = ""
_error.value = null
viewModelScope.launch {
try {
val messages = buildMessages(conv)
val request = ChatRequest(
model = conv.model,
messages = messages,
stream = true,
temperature = _temperature.value,
maxTokens = _maxTokens.value
)
val responseBody = withContext(Dispatchers.IO) {
RetrofitClient.api.chatCompletionsStream(request)
}
val reader = BufferedReader(InputStreamReader(responseBody.byteStream()))
val fullContent = StringBuilder()
try {
var line: String?
while (reader.readLine().also { line = it } != null) {
val l = line ?: continue
if (!l.startsWith("data: ")) continue
val data = l.removePrefix("data: ").trim()
if (data == "[DONE]") break
try {
val chunk = gson.fromJson(data, StreamChunk::class.java)
val delta = chunk.choices?.firstOrNull()?.delta
if (delta?.content != null) {
fullContent.append(delta.content)
_streamingContent.value = fullContent.toString()
}
} catch (_: Exception) {}
}
} finally {
reader.close()
responseBody.close()
}
if (fullContent.isNotEmpty()) {
conv.messages.add(ChatMessage(role = "assistant", content = fullContent.toString()))
updateConversation(conv.copy())
}
_streamingContent.value = ""
} catch (e: Exception) {
_error.value = "流式请求失败: ${e.message}"
_streamingContent.value = ""
} finally {
_isStreaming.value = false
}
}
}
private fun buildMessages(conv: Conversation): List<ChatMessage> {
val messages = mutableListOf<ChatMessage>()
if (conv.systemPrompt.isNotBlank()) {
messages.add(ChatMessage(role = "system", content = conv.systemPrompt))
}
messages.addAll(conv.messages)
return messages
}
private fun updateConversation(conv: Conversation) {
_currentConversation.value = conv
val list = _conversations.value.toMutableList()
val idx = list.indexOfFirst { it.id == conv.id }
if (idx >= 0) {
list[idx] = conv
}
_conversations.value = list
saveConversations()
}
fun clearError() {
_error.value = null
}
}
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/mimo_assistant_bubble" />
<corners
android:topLeftRadius="4dp"
android:topRightRadius="16dp"
android:bottomLeftRadius="16dp"
android:bottomRightRadius="16dp" />
</shape>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/mimo_surface" />
<corners android:radius="12dp" />
<stroke
android:width="1dp"
android:color="@color/divider" />
</shape>
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/mimo_input_bg" />
<corners android:radius="24dp" />
<padding
android:left="16dp"
android:top="8dp"
android:right="16dp"
android:bottom="8dp" />
</shape>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/mimo_primary" />
</shape>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/mimo_user_bubble" />
<corners
android:topLeftRadius="16dp"
android:topRightRadius="16dp"
android:bottomLeftRadius="16dp"
android:bottomRightRadius="4dp" />
</shape>
@@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#FFFFFF">
<group android:translateX="22" android:translateY="22">
<path
android:fillColor="@android:color/white"
android:pathData="M32,0C14.32,0,0,14.32,0,32s14.32,32,32,32s32,-14.32,32,-32S49.68,0,32,0zM32,58c-14.34,0,-26,-11.66,-26,-26
S17.66,6,32,6s26,11.66,26,26S46.34,58,32,58zM44,34H34v10c0,1.1,-0.9,2,-2,2s-2,-0.9,-2,-2V34H20c-1.1,0,-2,-0.9,-2,-2
s0.9,-2,2,-2h10V20c0,-1.1,0.9,-2,2,-2s2,0.9,2,2v10h10c1.1,0,2,0.9,2,2S45.1,34,44,34z" />
</group>
</vector>
-146
View File
@@ -1,146 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
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:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<!-- Main content -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/mimo_bg">
<!-- Toolbar -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
android:background="@color/mimo_primary"
app:title="MiMo Studio"
app:titleTextColor="@color/white"
app:navigationIconTint="@color/white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Model selector -->
<LinearLayout
android:id="@+id/modelBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:background="@color/mimo_surface"
android:elevation="2dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="模型:"
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:layout_marginEnd="8dp" />
<Spinner
android:id="@+id/spinnerModel"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@drawable/bg_card" />
</LinearLayout>
<!-- Chat messages -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerChat"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="@id/modelBar"
app:layout_constraintBottom_toTopOf="@id/inputBar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Streaming indicator -->
<TextView
android:id="@+id/tvStreaming"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/mimo_assistant_bubble"
android:padding="12dp"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/inputBar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Input bar -->
<LinearLayout
android:id="@+id/inputBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp"
android:background="@color/mimo_surface"
android:elevation="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<EditText
android:id="@+id/etMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="输入消息..."
android:background="@drawable/bg_input"
android:maxLines="5"
android:inputType="textMultiLine"
android:textSize="15sp"
android:layout_marginEnd="8dp" />
<ImageButton
android:id="@+id/btnSend"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_send"
android:background="@drawable/bg_send_button"
android:contentDescription="发送"
android:padding="12dp"
android:scaleType="fitCenter" />
</LinearLayout>
<!-- Loading indicator -->
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Navigation drawer (sidebar) -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header"
app:menu="@menu/nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>
@@ -1,32 +0,0 @@
<?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="match_parent"
android:orientation="vertical"
android:background="@color/mimo_bg">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/mimo_primary"
app:title="可用模型"
app:titleTextColor="@color/white"
app:navigationIcon="@android:drawable/ic_menu_revert"
app:navigationIconTint="@color/white" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerModels"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>
-122
View File
@@ -1,122 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
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="API Key"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="4dp" />
<EditText
android:id="@+id/etApiKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入你的 API Key"
android:inputType="textPassword"
android:background="@drawable/bg_card"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Base URL"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp" />
<EditText
android:id="@+id/etBaseUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="https://api.xiaomimimo.com/"
android:inputType="textUri"
android:background="@drawable/bg_card"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="系统提示词"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp" />
<EditText
android:id="@+id/etSystemPrompt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="可选:设置助手的行为..."
android:inputType="textMultiLine"
android:minLines="2"
android:maxLines="4"
android:background="@drawable/bg_card"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="温度 (Temperature)"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<SeekBar
android:id="@+id/seekTemperature"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:max="200"
android:progress="100" />
<TextView
android:id="@+id/tvTemperature"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:text="1.0"
android:textAlignment="center"
android:layout_marginStart="8dp" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最大输出 Token"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp" />
<EditText
android:id="@+id/etMaxTokens"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="4096"
android:inputType="number"
android:background="@drawable/bg_card"
android:padding="12dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchStreaming"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="流式输出 (Streaming)"
android:checked="true"
android:layout_marginTop="16dp" />
</LinearLayout>
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="start"
android:paddingStart="8dp"
android:paddingEnd="48dp"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:maxWidth="320dp">
<TextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_assistant_bubble"
android:padding="12dp"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:textIsSelectable="true" />
<TextView
android:id="@+id/tvTimestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="2dp"
android:layout_marginStart="4dp" />
</LinearLayout>
</LinearLayout>
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end"
android:paddingStart="48dp"
android:paddingEnd="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<TextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_user_bubble"
android:padding="12dp"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:textIsSelectable="true" />
<TextView
android:id="@+id/tvTimestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="2dp"
android:layout_marginEnd="4dp" />
</LinearLayout>
-69
View File
@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
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:layout_margin="4dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/mimo_surface">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvModelName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:id="@+id/tvModelId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/mimo_primary"
android:layout_marginTop="2dp" />
<TextView
android:id="@+id/tvModality"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/tvContext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/text_secondary"
android:layout_marginTop="2dp" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textColor="@color/text_primary"
android:layout_marginTop="8dp"
android:maxLines="3"
android:ellipsize="end" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSelect"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="选择此模型"
android:textSize="12sp"
android:layout_marginTop="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="160dp"
android:orientation="vertical"
android:gravity="bottom"
android:padding="16dp"
android:background="@color/mimo_primary">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MiMo Studio"
android:textSize="22sp"
android:textColor="@color/white"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Xiaomi AI Platform"
android:textSize="14sp"
android:textColor="#B0FFFFFF"
android:layout_marginTop="4dp" />
</LinearLayout>
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_new_chat"
android:icon="@android:drawable/ic_menu_add"
android:title="新建对话" />
<item
android:id="@+id/nav_models"
android:icon="@android:drawable/ic_menu_info_details"
android:title="模型列表" />
<item
android:id="@+id/nav_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:title="设置" />
</menu>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/mimo_primary"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/mimo_primary"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/mimo_primary"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
-25
View File
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#FF6200EE</color>
<color name="primary_dark">#FF3700B3</color>
<color name="accent">#FF03DAC5</color>
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="bg_light">#FFF5F5F5</color>
<color name="bg_dark">#FF121212</color>
<color name="bg_chat_user">#FFE3F2FD</color>
<color name="bg_chat_assistant">#FFFFFFFF</color>
<color name="text_primary">#FF212121</color>
<color name="text_secondary">#FF757575</color>
<color name="divider">#FFE0E0E0</color>
<!-- MiMo brand colors -->
<color name="mimo_primary">#FF4A90D9</color>
<color name="mimo_primary_dark">#FF3A7BC8</color>
<color name="mimo_accent">#FFFF6B35</color>
<color name="mimo_bg">#FFF8F9FA</color>
<color name="mimo_surface">#FFFFFFFF</color>
<color name="mimo_user_bubble">#FFE8F0FE</color>
<color name="mimo_assistant_bubble">#FFF1F3F4</color>
<color name="mimo_input_bg">#FFF0F2F5</color>
</resources>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MiMo Studio</string>
</resources>
-24
View File
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.MimoStudio" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/mimo_primary</item>
<item name="colorPrimaryDark">@color/mimo_primary_dark</item>
<item name="colorAccent">@color/mimo_accent</item>
<item name="android:statusBarColor">@color/mimo_primary</item>
<item name="android:navigationBarColor">@color/mimo_surface</item>
</style>
<style name="RoundedButton" parent="Widget.Material3.Button">
<item name="cornerRadius">24dp</item>
</style>
<style name="ChatBubble.User" parent="">
<item name="android:background">@drawable/bg_user_bubble</item>
<item name="android:padding">12dp</item>
</style>
<style name="ChatBubble.Assistant" parent="">
<item name="android:background">@drawable/bg_assistant_bubble</item>
<item name="android:padding">12dp</item>
</style>
</resources>
-5
View File
@@ -1,5 +0,0 @@
// Top-level build file
plugins {
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.0.21" apply false
}
-4
View File
@@ -1,4 +0,0 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
Binary file not shown.
-7
View File
@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
-139
View File
@@ -1,139 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld -- "$app_path" )
link=${ls#*' -> '}
case $link in
/*) app_path=$link ;;
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in
CYGWIN* ) cygwin=true ;;
Darwin* ) darwin=true ;;
MSYS* | MINGW* ) msys=true ;;
NonStop* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1 ; then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
;;
esac
case $MAX_FD in
'' | soft) :;;
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
;;
esac
fi
# Collect all arguments for the java command, stracks://am/LF://the current directory://
# temporary://build://for://long://args://
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and/or backslashes, so put them in
# temporary variables so that the://shell://can://split://them://up (https://github.com/gradle/gradle/issues/25036)
# * Put://all://options://into://a://single://quoted://word://so://they://are://not://split
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xeli" is available and://the://GRADLE_OPTS does not contain://any://-J flags
if ! "$cygwin" && ! "$msys" && [ -n "$TERM" ] ; then
:
fi
exec "$JAVACMD" "$@"
-16
View File
@@ -1,16 +0,0 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolution {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MimoStudio"
include(":app")