feat: MiMo Studio Android v1.0.0
- OpenAI-compatible API client for Xiaomi MiMo platform
- Chat completions with streaming support
- Model list browser
- Multi-conversation management
- Settings: API key, base URL, temperature, max tokens
- Material Design 3 UI
API Endpoints:
- GET /v1/models - List available models
- GET /v1/models/{id} - Get model details
- POST /v1/chat/completions - Chat (stream & non-stream)
- POST /v1/completions - Text completions (stream & non-stream)
This commit is contained in:
commit
e1861a2967
38 files changed
+1892
No files matched your search
+11
@@ -0,0 +1,11 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
/tmp
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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
@@ -0,0 +1,5 @@
|
|||||||
|
-dontwarn okhttp3.**
|
||||||
|
-dontwarn okio.**
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
-keep class com.mimo.studio.model.** { *; }
|
||||||
|
-keep class com.mimo.studio.api.** { *; }
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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()
|
||||||
|
)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/mimo_primary"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/mimo_primary"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/mimo_primary"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">MiMo Studio</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
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
@@ -0,0 +1,7 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#!/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" "$@"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolution {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "MimoStudio"
|
||||||
|
include(":app")
|
||||||
Reference in new issue
Block a user