Files
flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt

433 lines
16 KiB
Kotlin
Raw Normal View History

2024-09-17 09:24:50 +08:00
package com.example.flomo_ai
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.text.method.PasswordTransformationMethod
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject
import java.util.*
// Data classes for the new settings structure
data class HeaderConfig(val key: String, val value: String)
data class PromptConfig(val id: String, val title: String, val content: String, val expanded: Boolean = false)
data class ButtonConfig(val id: String, val label: String, val action: String, val apiUrl: String? = null, val apiMethod: String? = null, val apiBodyTemplate: String? = null, val expanded: Boolean = false)
2024-09-17 09:24:50 +08:00
class SecondActivity : AppCompatActivity() {
// View references
private lateinit var etBaseUrl: EditText
private lateinit var etApiKey: EditText
private lateinit var btnToggleApiKey: ImageButton
private lateinit var etModel: EditText
private lateinit var llHeadersList: LinearLayout
private lateinit var btnAddHeader: Button
private lateinit var llPromptsList: LinearLayout
private lateinit var btnAddPrompt: Button
private lateinit var tvEmptyPrompts: TextView
private lateinit var llButtonsList: LinearLayout
private lateinit var btnAddButton: Button
private lateinit var tvEmptyButtons: TextView
// Data storage
private var headerConfigs = mutableListOf<HeaderConfig>()
private var promptConfigs = mutableListOf<PromptConfig>()
private var buttonConfigs = mutableListOf<ButtonConfig>()
// API config (for backward compatibility with existing API calls)
private lateinit var apiConfig: APIConfig
2024-09-17 09:24:50 +08:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
2024-09-17 09:24:50 +08:00
// Initialize views
2024-09-17 09:24:50 +08:00
initViews()
// Load existing configurations
loadConfigurations()
// Setup UI based on loaded data
setupUI()
// Back button functionality
findViewById<Button>(R.id.btnBack).setOnClickListener {
finish()
2024-09-17 09:24:50 +08:00
}
// Add header button
btnAddHeader.setOnClickListener {
addHeaderEntry()
2024-09-17 09:24:50 +08:00
}
// Add prompt button
btnAddPrompt.setOnClickListener {
addPromptEntry()
2024-11-07 21:31:57 +08:00
}
// Add button
btnAddButton.setOnClickListener {
addButtonEntry()
2024-11-07 21:31:57 +08:00
}
}
2024-09-17 09:24:50 +08:00
private fun initViews() {
etBaseUrl = findViewById(R.id.etBaseUrl)
etApiKey = findViewById(R.id.etApiKey)
btnToggleApiKey = findViewById(R.id.btnToggleApiKey)
etModel = findViewById(R.id.etModel)
llHeadersList = findViewById(R.id.llHeadersList)
btnAddHeader = findViewById(R.id.btnAddHeader)
llPromptsList = findViewById(R.id.llPromptsList)
btnAddPrompt = findViewById(R.id.btnAddPrompt)
tvEmptyPrompts = findViewById(R.id.tvEmptyPrompts)
llButtonsList = findViewById(R.id.llButtonsList)
btnAddButton = findViewById(R.id.btnAddButton)
tvEmptyButtons = findViewById(R.id.tvEmptyButtons)
// Setup API key toggle
btnToggleApiKey.setOnClickListener {
val isPassword = etApiKey.transformationMethod is PasswordTransformationMethod
etApiKey.transformationMethod = if (isPassword) null else PasswordTransformationMethod()
// Move cursor to end
etApiKey.setSelection(etApiKey.text.length)
// Toggle icon
val iconRes = if (isPassword)
android.R.drawable.ic_lock_idle_lock else
android.R.drawable.ic_lock_idle_unlocked
btnToggleApiKey.setImageResource(iconRes)
}
2024-09-17 09:24:50 +08:00
}
private fun loadConfigurations() {
// Load shared preferences
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
2024-09-17 09:24:50 +08:00
val json = sharedPrefs.getString("configs", null)
2024-09-17 09:24:50 +08:00
if (json != null) {
try {
// Try to load as new format first
val settings = Gson().fromJson(json, SettingsData::class.java)
headerConfigs = settings.headerConfigs?.toMutableList() ?: mutableListOf()
promptConfigs = settings.promptConfigs?.toMutableList() ?: mutableListOf()
buttonConfigs = settings.buttonConfigs?.toMutableList() ?: mutableListOf()
// Load LLM config
etBaseUrl.setText(settings.llmConfig?.baseUrl ?: "https://api.openai.com/v1")
etApiKey.setText(settings.llmConfig?.apiKey ?: "")
etModel.setText(settings.llmConfig?.model ?: "gpt-4o")
// Update API key visibility based on whether it has text
updateApiKeyVisibility()
} catch (e: Exception) {
// If new format fails, try to load old format for migration
try {
val type = object : TypeToken<List<APIConfig>>() {}.type
val oldConfigs = Gson().fromJson<List<APIConfig>>(json, type)
if (oldConfigs.isNotEmpty()) {
val oldConfig = oldConfigs[0]
etBaseUrl.setText(oldConfig.url)
etApiKey.setText(oldConfig.key)
etModel.setText(oldConfig.model)
updateApiKeyVisibility()
}
} catch (e2: Exception) {
// If both fail, use defaults
etBaseUrl.setText("https://api.openai.com/v1")
etModel.setText("gpt-4o")
updateApiKeyVisibility()
}
}
} else {
// No saved config, use defaults
etBaseUrl.setText("https://api.openai.com/v1")
etModel.setText("gpt-4o")
updateApiKeyVisibility()
2024-09-17 09:24:50 +08:00
}
}
private fun updateApiKeyVisibility() {
val isEmpty = etApiKey.text.toString().isEmpty()
etApiKey.transformationMethod = if (isEmpty) null else PasswordTransformationMethod()
val iconRes = if (isEmpty || etApiKey.transformationMethod == null)
android.R.drawable.ic_lock_idle_lock else
android.R.drawable.ic_lock_idle_unlocked
btnToggleApiKey.setImageResource(iconRes)
// Keep cursor at end
etApiKey.setSelection(etApiKey.text.length)
2024-09-17 09:24:50 +08:00
}
private fun setupUI() {
// Setup headers
llHeadersList.removeAllViews()
if (headerConfigs.isEmpty()) {
addHeaderEntry() // Add one empty entry by default
} else {
for (header in headerConfigs) {
addHeaderEntry(header.key, header.value)
}
}
// Setup prompts
llPromptsList.removeAllViews()
if (promptConfigs.isEmpty()) {
// Add default prompts from JSON
promptConfigs.add(PromptConfig("default-1", "翻译助手", "你是一个专业翻译,请将用户输入的内容翻译成中文,保持原意,语言自然流畅。"))
promptConfigs.add(PromptConfig("default-2", "代码解释", "你是一个资深程序员,请详细解释用户提供的代码,用中文说明其功能和逻辑。"))
}
for (prompt in promptConfigs) {
addPromptEntry(prompt)
}
updateEmptyStates()
// Setup buttons
llButtonsList.removeAllViews()
if (buttonConfigs.isEmpty()) {
// Add default buttons from JSON
buttonConfigs.add(ButtonConfig("btn-copy", "复制结果", "copy"))
buttonConfigs.add(ButtonConfig(
"btn-webhook",
"发送到飞书",
"api",
"https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx",
"POST",
"{\"msg_type\": \"text\", \"content\": {\"text\": \"{output}\"}}"
))
}
for (button in buttonConfigs) {
addButtonEntry(button)
}
updateEmptyStates()
2024-09-17 09:24:50 +08:00
}
private fun addHeaderEntry(key: String = "", value: String = "") {
val view = layoutInflater.inflate(R.layout.header_entry, null)
val etKey = view.findViewById<EditText>(R.id.etHeaderKey)
val etValue = view.findViewById<EditText>(R.id.etHeaderValue)
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemoveHeader)
etKey.setText(key)
etValue.setText(value)
btnRemove.setOnClickListener {
llHeadersList.removeView(view)
updateEmptyStates()
2024-09-17 09:24:50 +08:00
}
llHeadersList.addView(view)
updateEmptyStates()
2024-09-17 09:24:50 +08:00
}
private fun addPromptEntry(config: PromptConfig = PromptConfig("", "", "")) {
val view = layoutInflater.inflate(R.layout.prompt_entry, null)
val etTitle = view.findViewById<EditText>(R.id.etPromptTitle)
val etContent = view.findViewById<EditText>(R.id.etPromptContent)
val btnExpand = view.findViewById<Button>(R.id.btnExpandPrompt)
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemovePrompt)
val vDivider = view.findViewById<View>(R.id.viewDivider)
etTitle.setText(config.title)
etContent.setText(config.content)
// Set expanded state (we'd need to store this in the view tag or similar)
btnRemove.setOnClickListener {
llPromptsList.removeView(view)
updateEmptyStates()
}
2024-09-17 09:24:50 +08:00
// For simplicity, we're not implementing expand/collapse here
// but in a full implementation we would toggle the content visibility
llPromptsList.addView(view)
updateEmptyStates()
}
private fun addButtonEntry(config: ButtonConfig = ButtonConfig("", "", "")) {
val view = layoutInflater.inflate(R.layout.button_entry, null)
val etLabel = view.findViewById<EditText>(R.id.etButtonLabel)
val btnAction = view.findViewById<Button>(R.id.btnButtonAction)
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemoveButton)
val llApiFields = view.findViewById<LinearLayout>(R.id.llApiFields)
etLabel.setText(config.label)
btnAction.text = when (config.action) {
"copy" -> "复制输出内容"
"api" -> "提交到第三方 API"
else -> "未知操作"
}
// Show/hide API fields based on action
llApiFields.visibility = if (config.action == "api") View.VISIBLE else View.GONE
btnAction.setOnClickListener {
// Cycle through action options
val newAction = when (btnAction.text.toString()) {
"复制输出内容" -> "提交到第三方 API"
"提交到第三方 API" -> "未知操作"
else -> "复制输出内容"
}
btnAction.text = when (newAction) {
"复制输出内容" -> "复制输出内容"
"提交到第三方 API" -> "提交到第三方 API"
else -> "未知操作"
2024-09-17 09:24:50 +08:00
}
llApiFields.visibility = if (newAction == "提交到第三方 API") View.VISIBLE else View.GONE
}
2024-09-17 09:24:50 +08:00
// If we have existing API config, populate fields
if (config.action == "api") {
// In a full implementation, we would populate the API fields here
2024-09-17 09:24:50 +08:00
}
btnRemove.setOnClickListener {
llButtonsList.removeView(view)
updateEmptyStates()
}
llButtonsList.addView(view)
updateEmptyStates()
2024-09-17 09:24:50 +08:00
}
private fun updateEmptyStates() {
// Update prompts empty state
tvEmptyPrompts.visibility = if (llPromptsList.childCount == 0) View.VISIBLE else View.GONE
// Update buttons empty state
tvEmptyButtons.visibility = if (llButtonsList.childCount == 0) View.VISIBLE else View.GONE
2024-09-17 09:24:50 +08:00
}
// Save all configurations when leaving or explicitly saving
override fun onPause() {
super.onPause()
saveConfigurations()
2024-09-17 09:24:50 +08:00
}
private fun saveConfigurations() {
// Update header configs from UI
headerConfigs.clear()
for (i in 0 until llHeadersList.childCount) {
val view = llHeadersList.getChildAt(i)
val key = view.findViewById<EditText>(R.id.etHeaderKey).text.toString()
val value = view.findViewById<EditText>(R.id.etHeaderValue).text.toString()
if (key.isNotBlank() && value.isNotBlank()) {
headerConfigs.add(HeaderConfig(key, value))
}
}
// Update prompt configs from UI
promptConfigs.clear()
for (i in 0 until llPromptsList.childCount) {
val view = llPromptsList.getChildAt(i)
val title = view.findViewById<EditText>(R.id.etPromptTitle).text.toString()
val content = view.findViewById<EditText>(R.id.etPromptContent).text.toString()
if (title.isNotBlank() && content.isNotBlank()) {
promptConfigs.add(PromptConfig(
if (title.equals("翻译助手", ignoreCase = true)) "default-1"
else if (title.equals("代码解释", ignoreCase = true)) "default-2"
else UUID.randomUUID().toString(),
title,
content
))
}
}
// Update button configs from UI
buttonConfigs.clear()
for (i in 0 until llButtonsList.childCount) {
val view = llButtonsList.getChildAt(i)
val label = view.findViewById<EditText>(R.id.etButtonLabel).text.toString()
val actionText = view.findViewById<Button>(R.id.btnButtonAction).text.toString()
val action = when (actionText) {
"复制输出内容" -> "copy"
"提交到第三方 API" -> "api"
else -> "unknown"
}
if (label.isNotBlank()) {
buttonConfigs.add(ButtonConfig(
if (label.equals("复制结果", ignoreCase = true)) "btn-copy"
else if (label.equals("发送到飞书", ignoreCase = true)) "btn-webhook"
else UUID.randomUUID().toString(),
label,
action
))
}
}
// Save LLM config
val llmConfig = LLMConfig(
baseUrl = etBaseUrl.text.toString(),
apiKey = etApiKey.text.toString(),
model = etModel.text.toString()
)
// Save everything
val settingsData = SettingsData(
llmConfig = llmConfig,
headerConfigs = headerConfigs,
promptConfigs = promptConfigs,
buttonConfigs = buttonConfigs
)
val json = Gson().toJson(settingsData)
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
sharedPrefs.edit().putString("configs", json).apply()
// Also update the legacy APIConfig for backward compatibility
apiConfig = APIConfig(
System.currentTimeMillis(),
"llm-config",
etBaseUrl.text.toString(),
etApiKey.text.toString(),
"",
etModel.text.toString()
)
}
2024-09-17 09:24:50 +08:00
// Legacy APIConfig class for backward compatibility with existing code
data class APIConfig(
val id: Long,
val name: String,
val url: String,
val key: String,
val secretKey: String,
val model: String
)
// New data classes for settings structure
data class LLMConfig(
val baseUrl: String,
val apiKey: String,
val model: String
)
data class SettingsData(
val llmConfig: LLMConfig?,
val headerConfigs: List<HeaderConfig>?,
val promptConfigs: List<PromptConfig>?,
val buttonConfigs: List<ButtonConfig>?
)
}