Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4f17a8828 | ||
|
|
125897b87b | ||
|
|
d73dd2d56f |
No files matched your search
-11
@@ -1,11 +0,0 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/tmp
|
||||
@@ -0,0 +1,17 @@
|
||||
# 项目已迁移 ✨
|
||||
|
||||
你好呀~这个仓库已经搬到更好用的地方啦。
|
||||
|
||||
为了让代码管理更清爽、多平台同步更省心,我们把原先分散的镜像统一收拢到主仓维护,这里就只保留这张"迁移小纸条"咯。
|
||||
|
||||
📦 新家在这里:
|
||||
- Gitee:https://gitee.com/shuiyu1123/mimo-studio-android
|
||||
- GitHub:https://github.com/shuiyu67/mimo-studio-android
|
||||
|
||||
📋 内部评估:
|
||||
- 镜像分散在多个平台,重复维护成本高、容易漏更;
|
||||
- 自托管 Gitea 实例为个人非管理员环境,长期可用性与备份缺乏保障;
|
||||
- 代码已稳定收拢至 Gitee / GitHub 主仓,原镜像不再承载更新职责;
|
||||
- 统一主仓后便于协作、CI 与备份,减少多端同步负担。
|
||||
|
||||
旧镜像不再更新,但代码都好好的在新家继续生长。感谢你一路的关注与陪伴 💛
|
||||
@@ -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")
|
||||
}
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.**
|
||||
-keep class com.mimo.studio.model.** { *; }
|
||||
-keep class com.mimo.studio.api.** { *; }
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">MiMo Studio</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
Vendored
BIN
Binary file not shown.
-7
@@ -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
|
||||
@@ -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" "$@"
|
||||
@@ -1,16 +0,0 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolution {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "MimoStudio"
|
||||
include(":app")
|
||||
Reference in new issue
Block a user