Compare commits

...

4 Commits

8 changed files with 1137 additions and 877 deletions

View File

@@ -1,6 +1,6 @@
# Flomo AI # Flomo AI
一个 AI 文本优化工具,包含 Android 原版和 Rust Windows 桌面移植版。 一个 AI 文本优化工具,支持 Android 原版和 Rust Windows 桌面版。
## 项目结构 ## 项目结构
@@ -11,7 +11,6 @@
│ │ │ └── com/example/flomo_ai/ │ │ │ └── com/example/flomo_ai/
│ │ │ ├── MainActivity.kt # 主界面 │ │ │ ├── MainActivity.kt # 主界面
│ │ │ ├── SecondActivity.kt # 设置页面 │ │ │ ├── SecondActivity.kt # 设置页面
│ │ │ ├── kwt.kt # JWT 生成工具
│ │ │ └── ui/theme/ # 主题管理 │ │ │ └── ui/theme/ # 主题管理
│ │ └── res/ # 布局、颜色、图标等资源 │ │ └── res/ # 布局、颜色、图标等资源
│ └── mumu-pytest/ # MuMu 模拟器自动化测试 │ └── mumu-pytest/ # MuMu 模拟器自动化测试
@@ -26,10 +25,7 @@
│ │ └── llm_client.rs # LLM API 调用OpenAI 兼容格式) │ │ └── llm_client.rs # LLM API 调用OpenAI 兼容格式)
│ ├── theme/ │ ├── theme/
│ │ └── mod.rs # 明暗主题管理 │ │ └── mod.rs # 明暗主题管理
── pages/ ── pages/ # 页面组件
│ │ ├── main_page.rs # 主界面组件
│ │ └── settings_page.rs # 设置页组件
│ └── widgets/ # 可复用 UI 组件
├── .cargo/config.toml # MinGW 工具链配置 ├── .cargo/config.toml # MinGW 工具链配置
└── Cargo.toml # 依赖声明 └── Cargo.toml # 依赖声明
``` ```
@@ -37,7 +33,8 @@
## 功能特性 ## 功能特性
### 核心功能 ### 核心功能
- **多模型配置**:支持添加、编辑、删除多个大模型配置,首页可快速切换 - **多模型并发**:支持同时配置 3 个 LLM 模型,发送时并发调用所有启用的模型
- **独立测试**:每个模型可单独测试连接状态
- **LLM API 调用**:兼容 OpenAI 格式的 API支持自定义 Base URL、API Key、Model - **LLM API 调用**:兼容 OpenAI 格式的 API支持自定义 Base URL、API Key、Model
- **自定义请求头**:可添加额外的 HTTP Header - **自定义请求头**:可添加额外的 HTTP Header
- **提示词管理**:添加、删除、编辑系统提示词,主界面下拉选择 - **提示词管理**:添加、删除、编辑系统提示词,主界面下拉选择
@@ -45,15 +42,18 @@
- **主题切换**:浅色模式 / 深色模式 / 跟随系统 - **主题切换**:浅色模式 / 深色模式 / 跟随系统
- **配置持久化**JSON 格式保存到用户配置目录 - **配置持久化**JSON 格式保存到用户配置目录
- **复制结果**:一键复制优化结果到剪贴板 - **复制结果**:一键复制优化结果到剪贴板
- **笔记保存**:支持 Flomo、Notion、Joplin 等笔记 APIAPI Key 可选)
### 界面布局 ### 界面布局
- **顶部标题栏**:显示 "AI优化" + 模型选择下拉框 + 配置按钮 - **主界面**
- **快速操作区**:四个 emoji 快捷按钮 - 顶部标题栏 + 配置按钮
- **提示词选择器**:下拉菜单选择系统提示词,显示提示词内容预览 - 提示词选择器 + 快速操作按钮
- **输入区域**多行文本输入自动增长3-10行限制 - 输入区域 + 发送按钮
- **发送/停止**:发送按钮 + 生成中可停止 - **三栏结果显示**:水平排列 3 个模型的结果卡片
- **结果展示区**:状态标签 + 可编辑结果文本 + 复制按钮 + 保存笔记按钮 - **配置页面**
- 3 个模型的独立配置启用开关、名称、URL、Key、Model
- 每个模型独立的测试按钮
- 主题设置
- 提示词管理
## 构建说明 ## 构建说明
@@ -74,7 +74,9 @@ cd flomo-ai-desktop
cargo build --release cargo build --release
``` ```
编译产物:`target/release/flomo-ai.exe`~4.4MB,无控制台窗口) 或使用 `build.bat`(需先安装 MSYS2 + MinGW
编译产物:`target/release/flomo-ai.exe`~4MB无控制台窗口
### 编译配置 ### 编译配置
- `.cargo/config.toml`:指定 MinGW 链接器和 `-mwindows` 参数 - `.cargo/config.toml`:指定 MinGW 链接器和 `-mwindows` 参数
@@ -83,8 +85,8 @@ cargo build --release
### 依赖 ### 依赖
| 依赖 | 用途 | | 依赖 | 用途 |
|------|------| |------|------|
| `egui` + `eframe` | GUI 框架(纯 Rust无 C 依赖) | | `egui` + `eframe` (glow) | GUI 框架(纯 Rust无 C 依赖) |
| `reqwest` (blocking + rustls-tls) | HTTP 客户端(纯 Rust TLS | | `reqwest` (blocking + native-tls) | HTTP 客户端 |
| `serde` + `serde_json` | 序列化 / 配置持久化 | | `serde` + `serde_json` | 序列化 / 配置持久化 |
| `dirs` | 获取用户配置目录路径 | | `dirs` | 获取用户配置目录路径 |
| `arboard` | 剪贴板操作 | | `arboard` | 剪贴板操作 |
@@ -95,11 +97,31 @@ cargo build --release
```json ```json
{ {
"llm_config": { "llm_configs": {
"models": [
{
"enabled": true,
"name": "模型1",
"base_url": "https://api.openai.com/v1", "base_url": "https://api.openai.com/v1",
"api_key": "", "api_key": "",
"model": "gpt-4o" "model": "gpt-4o"
}, },
{
"enabled": false,
"name": "模型2",
"base_url": "",
"api_key": "",
"model": ""
},
{
"enabled": false,
"name": "模型3",
"base_url": "",
"api_key": "",
"model": ""
}
]
},
"header_configs": [], "header_configs": [],
"prompt_configs": [ "prompt_configs": [
{ {

View File

@@ -288,79 +288,78 @@ impl FlomoAiApp {
ui.add_space(6.0); ui.add_space(6.0);
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = (available_width - 10.0) / 3.0; let column_width = (available_width - 16.0) / 3.0;
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.set_height(200.0);
for (i, display) in self.model_displays.iter().enumerate() { for (i, display) in self.model_displays.iter().enumerate() {
if i > 0 { if i > 0 {
ui.add_space(5.0); ui.add_space(8.0);
} }
ui.set_min_width(column_width); ui.set_min_width(column_width);
ui.set_max_width(column_width);
ui.vertical(|ui| {
ui.set_width(column_width);
let bg_color = if display.enabled {
ui.style().visuals.widgets.inactive.bg_fill
} else {
egui::Color32::from_rgb(240, 240, 240)
};
egui::Frame::none() egui::Frame::none()
.fill(bg_color) .fill(ui.style().visuals.widgets.inactive.bg_fill)
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 210))) .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(180, 180, 200)))
.rounding(6.0) .rounding(8.0)
.inner_margin(egui::Margin::same(8.0)) .inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| { .show(ui, |ui| {
let status_color = match &display.status { ui.set_min_width(column_width - 20.0);
ModelStatus::Waiting => egui::Color32::GRAY,
ModelStatus::Loading => egui::Color32::from_rgb(255, 165, 0), let enabled_color = if display.enabled {
ModelStatus::Completed => egui::Color32::from_rgb(0, 180, 0), egui::Color32::from_rgb(80, 80, 220)
ModelStatus::Error(_) => egui::Color32::RED, } else {
egui::Color32::GRAY
}; };
let enabled_dot = if display.enabled { "" } else { "" }; let enabled_dot = if display.enabled { "" } else { "" };
ui.label(egui::RichText::new(format!("{} {}", enabled_dot, display.name)) ui.label(egui::RichText::new(format!("{} {}", enabled_dot, display.name))
.size(12.0).strong() .size(13.0).strong().color(enabled_color));
.color(if display.enabled { egui::Color32::from_rgb(100, 100, 255) } else { egui::Color32::GRAY }));
ui.label(egui::RichText::new(&display.model).size(10.0).color(egui::Color32::GRAY)); ui.label(egui::RichText::new(&display.model).size(10.0).color(egui::Color32::GRAY));
ui.add_space(4.0); ui.add_space(6.0);
ui.add_sized([ui.available_width(), 1.0], egui::Separator::default()); ui.add_sized([ui.available_width(), 1.0], egui::Separator::default());
ui.add_space(6.0);
let status_color = match &display.status {
ModelStatus::Waiting => egui::Color32::GRAY,
ModelStatus::Loading => egui::Color32::from_rgb(255, 140, 0),
ModelStatus::Completed => egui::Color32::from_rgb(0, 160, 0),
ModelStatus::Error(_) => egui::Color32::RED,
};
ui.label(egui::RichText::new(match &display.status {
ModelStatus::Waiting => "● 就绪",
ModelStatus::Loading => "◐ 生成中...",
ModelStatus::Completed => "✓ 完成",
ModelStatus::Error(_) => "✗ 错误",
}).size(10.0).color(status_color));
ui.add_space(4.0); ui.add_space(4.0);
if display.result.is_empty() { if display.result.is_empty() {
ui.label(egui::RichText::new("等待结果...").size(11.0).color(egui::Color32::GRAY)); ui.label(egui::RichText::new("等待结果...").size(11.0).color(egui::Color32::GRAY));
} else { } else {
let mut result_text = display.result.clone();
ui.add_sized( ui.add_sized(
[ui.available_width(), 120.0], [ui.available_width(), 120.0],
egui::TextEdit::multiline(&mut display.result.clone()) egui::TextEdit::multiline(&mut result_text)
.desired_rows(5) .desired_rows(5)
.frame(false), .frame(false),
); );
} }
ui.add_space(4.0); ui.add_space(6.0);
ui.add_sized([ui.available_width(), 1.0], egui::Separator::default());
ui.add_space(4.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(egui::RichText::new(match &display.status {
ModelStatus::Waiting => "就绪",
ModelStatus::Loading => "生成中...",
ModelStatus::Completed => "完成",
ModelStatus::Error(_) => "错误",
}).size(10.0).color(status_color));
if ui.small_button("复制").clicked() && !display.result.is_empty() { if ui.small_button("复制").clicked() && !display.result.is_empty() {
self.copy_to_clipboard(&display.result); self.copy_to_clipboard(&display.result);
} }
}); });
}); });
});
} }
}); });
}); });

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
@@ -31,13 +32,19 @@ import org.json.JSONObject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var inputEditText: EditText private lateinit var inputEditText: EditText
private lateinit var outputTextView: EditText private lateinit var tvResult1: TextView
private lateinit var tvResult2: TextView
private lateinit var tvResult3: TextView
private lateinit var tvStatus1: TextView
private lateinit var tvStatus2: TextView
private lateinit var tvStatus3: TextView
private lateinit var promptSelector: Spinner private lateinit var promptSelector: Spinner
private lateinit var promptNameText: TextView private lateinit var promptNameText: TextView
private lateinit var promptContentText: TextView private lateinit var promptContentText: TextView
private var llmConfigs = listOf<LLMConfig>() private var llmConfigs = listOf<LLMConfig>()
private var selectedLlmIndex = 0 private var selectedLlmIndex = 0
private val results = mutableMapOf<Int, String>()
@SuppressLint("MissingInflatedId", "CutPasteId", "SetTextI18n") @SuppressLint("MissingInflatedId", "CutPasteId", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -51,6 +58,30 @@ class MainActivity : AppCompatActivity() {
inputEditText = findViewById<EditText>(R.id.inputEditText) inputEditText = findViewById<EditText>(R.id.inputEditText)
val sendButton = findViewById<Button>(R.id.sendButton) val sendButton = findViewById<Button>(R.id.sendButton)
val btnCopyResult = findViewById<Button>(R.id.btnCopyResult) val btnCopyResult = findViewById<Button>(R.id.btnCopyResult)
val configButton = findViewById<Button>(R.id.configButton)
val modelSelector = findViewById<Spinner>(R.id.headerModelSelector)
// 初始化三栏结果视图
tvResult1 = findViewById(R.id.tvResult1)
tvResult2 = findViewById(R.id.tvResult2)
tvResult3 = findViewById(R.id.tvResult3)
tvStatus1 = findViewById(R.id.tvStatus1)
tvStatus2 = findViewById(R.id.tvStatus2)
tvStatus3 = findViewById(R.id.tvStatus3)
// 配置按钮点击事件
configButton.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
// 模型选择器
modelSelector.setOnItemSelectedListener(object : android.widget.AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: android.widget.AdapterView<*>, view: View?, position: Int, id: Long) {
selectedLlmIndex = position
}
override fun onNothingSelected(parent: android.widget.AdapterView<*>) {}
})
loadModelsFromConfig() loadModelsFromConfig()
loadPromptsFromConfig() loadPromptsFromConfig()
@@ -104,8 +135,13 @@ class MainActivity : AppCompatActivity() {
} }
btnCopyResult.setOnClickListener { btnCopyResult.setOnClickListener {
val textToCopy = outputTextView.text.toString() val sb = StringBuilder()
if (textToCopy.isNotEmpty() && textToCopy != "发送消息后结果将在此显示") { if (results.containsKey(0)) sb.append("模型1:\n${results[0]}\n\n")
if (results.containsKey(1)) sb.append("模型2:\n${results[1]}\n\n")
if (results.containsKey(2)) sb.append("模型3:\n${results[2]}\n\n")
val textToCopy = sb.toString().trim()
if (textToCopy.isNotEmpty()) {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("优化结果", textToCopy) val clip = ClipData.newPlainText("优化结果", textToCopy)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
@@ -118,6 +154,11 @@ class MainActivity : AppCompatActivity() {
initQuickButtons() initQuickButtons()
} }
override fun onResume() {
super.onResume()
loadModelsFromConfig()
}
private fun loadModelsFromConfig() { private fun loadModelsFromConfig() {
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE) val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null) val json = sharedPrefs.getString("configs", null)
@@ -131,6 +172,19 @@ class MainActivity : AppCompatActivity() {
Log.e("MainActivity", "Error loading config", e) Log.e("MainActivity", "Error loading config", e)
} }
} }
// 更新模型选择器
val modelSelector = findViewById<Spinner>(R.id.headerModelSelector)
val modelNames = llmConfigs.filter { it.enabled }.map { it.name }.toMutableList()
if (modelNames.isEmpty()) {
modelNames.add("未配置模型")
}
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, modelNames)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
modelSelector.adapter = adapter
if (selectedLlmIndex < modelNames.size) {
modelSelector.setSelection(selectedLlmIndex)
}
} }
private fun loadLlmConfigsAndSend(content: String) { private fun loadLlmConfigsAndSend(content: String) {
@@ -143,22 +197,49 @@ class MainActivity : AppCompatActivity() {
if (configs.isEmpty()) return if (configs.isEmpty()) return
val config = if (selectedLlmIndex < configs.size) configs[selectedLlmIndex] else configs[0] // 找出所有启用的模型
val baseUrl = config.baseUrl.ifEmpty { "https://api.openai.com/v1" } val enabledConfigs = configs.filter { it.enabled && it.baseUrl.isNotEmpty() && it.model.isNotEmpty() }
val apiKey = config.apiKey
val model = config.model.ifEmpty { "gpt-4o" }
sendToLlm(baseUrl, apiKey, model, content) if (enabledConfigs.isEmpty()) {
Toast.makeText(this, "没有启用的大模型配置", Toast.LENGTH_SHORT).show()
return
}
// 初始化所有结果显示
results.clear()
tvStatus1.text = "等待..."
tvStatus2.text = "等待..."
tvStatus3.text = "等待..."
tvResult1.text = "等待结果..."
tvResult2.text = "等待结果..."
tvResult3.text = "等待结果..."
// 为每个启用的模型启动请求
enabledConfigs.forEachIndexed { index, config ->
sendToLlm(config.baseUrl, config.apiKey, config.model, content, index, enabledConfigs.size)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MainActivity", "Error loading LLM config", e) Log.e("MainActivity", "Error loading LLM config", e)
outputTextView.setText("错误: ${e.message}") Toast.makeText(this, "配置加载错误: ${e.message}", Toast.LENGTH_SHORT).show()
} }
} }
private fun sendToLlm(baseUrl: String, apiKey: String, model: String, content: String) { private fun sendToLlm(baseUrl: String, apiKey: String, model: String, content: String, index: Int, total: Int) {
val outputStatusLabel = findViewById<TextView>(R.id.outputStatusLabel) val statusView = when (index) {
outputStatusLabel.text = "连接中..." 0 -> tvStatus1
outputTextView.setText("正在生成...") 1 -> tvStatus2
2 -> tvStatus3
else -> tvStatus1
}
val resultView = when (index) {
0 -> tvResult1
1 -> tvResult2
2 -> tvResult3
else -> tvResult1
}
statusView.text = "生成中..."
resultView.text = "正在生成..."
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
@@ -198,21 +279,22 @@ class MainActivity : AppCompatActivity() {
if (choices.length() > 0) { if (choices.length() > 0) {
val message = choices.getJSONObject(0).getJSONObject("message") val message = choices.getJSONObject(0).getJSONObject("message")
val result = message.getString("content") val result = message.getString("content")
outputStatusLabel.text = "完成" statusView.text = "完成"
outputTextView.setText(result) resultView.text = result
results[index] = result
} else { } else {
outputStatusLabel.text = "发生错误" statusView.text = "错误"
outputTextView.setText("API 返回空结果") resultView.text = "API 返回空结果"
} }
} else { } else {
outputStatusLabel.text = "发生错误" statusView.text = "错误"
outputTextView.setText("API 错误: ${response.code} ${response.message}") resultView.text = "API 错误: ${response.code} ${response.message}"
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
outputStatusLabel.text = "发生错误" statusView.text = "错误"
outputTextView.setText("错误: ${e.message}") resultView.text = "错误: ${e.message}"
} }
} }
} }

View File

@@ -2,13 +2,22 @@ package com.example.flomo_ai
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.text.method.PasswordTransformationMethod import android.text.method.PasswordTransformationMethod
import android.util.Log import android.view.LayoutInflater
import android.view.View
import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.example.flomo_ai.ui.theme.ThemeManager import com.example.flomo_ai.ui.theme.ThemeManager
@@ -24,31 +33,42 @@ import okhttp3.RequestBody.Companion.toRequestBody
class SecondActivity : AppCompatActivity() { class SecondActivity : AppCompatActivity() {
private lateinit var cbModel1Enabled: CheckBox
private lateinit var etModelName1: EditText
private lateinit var etBaseUrl1: EditText private lateinit var etBaseUrl1: EditText
private lateinit var etApiKey1: EditText private lateinit var etApiKey1: EditText
private lateinit var btnToggleApiKey1: ImageButton private lateinit var btnToggleApiKey1: ImageButton
private lateinit var etModel1: EditText private lateinit var etModel1: EditText
private lateinit var etModelName1: EditText
private lateinit var btnTestConnection1: Button private lateinit var btnTestConnection1: Button
private lateinit var tvTestStatus1: TextView private lateinit var tvTestStatus1: TextView
private lateinit var cbModel2Enabled: CheckBox
private lateinit var etModelName2: EditText
private lateinit var etBaseUrl2: EditText private lateinit var etBaseUrl2: EditText
private lateinit var etApiKey2: EditText private lateinit var etApiKey2: EditText
private lateinit var btnToggleApiKey2: ImageButton private lateinit var btnToggleApiKey2: ImageButton
private lateinit var etModel2: EditText private lateinit var etModel2: EditText
private lateinit var etModelName2: EditText
private lateinit var btnTestConnection2: Button private lateinit var btnTestConnection2: Button
private lateinit var tvTestStatus2: TextView private lateinit var tvTestStatus2: TextView
private lateinit var cbModel3Enabled: CheckBox
private lateinit var etModelName3: EditText
private lateinit var etBaseUrl3: EditText private lateinit var etBaseUrl3: EditText
private lateinit var etApiKey3: EditText private lateinit var etApiKey3: EditText
private lateinit var btnToggleApiKey3: ImageButton private lateinit var btnToggleApiKey3: ImageButton
private lateinit var etModel3: EditText private lateinit var etModel3: EditText
private lateinit var etModelName3: EditText
private lateinit var btnTestConnection3: Button private lateinit var btnTestConnection3: Button
private lateinit var tvTestStatus3: TextView private lateinit var tvTestStatus3: TextView
private lateinit var promptContainer: LinearLayout
private lateinit var btnAddPrompt: Button
private lateinit var rgTheme: RadioGroup
private lateinit var rbThemeLight: RadioButton
private lateinit var rbThemeDark: RadioButton
private lateinit var rbThemeAuto: RadioButton
private var llmConfigs = mutableListOf<LLMConfig>() private var llmConfigs = mutableListOf<LLMConfig>()
private var promptConfigs = mutableListOf<PromptConfig>()
private var selectedLlmIndex = 0 private var selectedLlmIndex = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -58,6 +78,7 @@ class SecondActivity : AppCompatActivity() {
initViews() initViews()
loadConfigurations() loadConfigurations()
updatePromptList()
findViewById<ImageButton>(R.id.btnBack).setOnClickListener { findViewById<ImageButton>(R.id.btnBack).setOnClickListener {
finish() finish()
@@ -65,30 +86,59 @@ class SecondActivity : AppCompatActivity() {
} }
private fun initViews() { private fun initViews() {
cbModel1Enabled = findViewById(R.id.cbModel1Enabled)
etModelName1 = findViewById(R.id.etModelName1)
etBaseUrl1 = findViewById(R.id.etBaseUrl1) etBaseUrl1 = findViewById(R.id.etBaseUrl1)
etApiKey1 = findViewById(R.id.etApiKey1) etApiKey1 = findViewById(R.id.etApiKey1)
btnToggleApiKey1 = findViewById(R.id.btnToggleApiKey1) btnToggleApiKey1 = findViewById(R.id.btnToggleApiKey1)
etModel1 = findViewById(R.id.etModel1) etModel1 = findViewById(R.id.etModel1)
etModelName1 = findViewById(R.id.etModelName1)
btnTestConnection1 = findViewById(R.id.btnTestConnection1) btnTestConnection1 = findViewById(R.id.btnTestConnection1)
tvTestStatus1 = findViewById(R.id.tvTestStatus1) tvTestStatus1 = findViewById(R.id.tvTestStatus1)
cbModel2Enabled = findViewById(R.id.cbModel2Enabled)
etModelName2 = findViewById(R.id.etModelName2)
etBaseUrl2 = findViewById(R.id.etBaseUrl2) etBaseUrl2 = findViewById(R.id.etBaseUrl2)
etApiKey2 = findViewById(R.id.etApiKey2) etApiKey2 = findViewById(R.id.etApiKey2)
btnToggleApiKey2 = findViewById(R.id.btnToggleApiKey2) btnToggleApiKey2 = findViewById(R.id.btnToggleApiKey2)
etModel2 = findViewById(R.id.etModel2) etModel2 = findViewById(R.id.etModel2)
etModelName2 = findViewById(R.id.etModelName2)
btnTestConnection2 = findViewById(R.id.btnTestConnection2) btnTestConnection2 = findViewById(R.id.btnTestConnection2)
tvTestStatus2 = findViewById(R.id.tvTestStatus2) tvTestStatus2 = findViewById(R.id.tvTestStatus2)
cbModel3Enabled = findViewById(R.id.cbModel3Enabled)
etModelName3 = findViewById(R.id.etModelName3)
etBaseUrl3 = findViewById(R.id.etBaseUrl3) etBaseUrl3 = findViewById(R.id.etBaseUrl3)
etApiKey3 = findViewById(R.id.etApiKey3) etApiKey3 = findViewById(R.id.etApiKey3)
btnToggleApiKey3 = findViewById(R.id.btnToggleApiKey3) btnToggleApiKey3 = findViewById(R.id.btnToggleApiKey3)
etModel3 = findViewById(R.id.etModel3) etModel3 = findViewById(R.id.etModel3)
etModelName3 = findViewById(R.id.etModelName3)
btnTestConnection3 = findViewById(R.id.btnTestConnection3) btnTestConnection3 = findViewById(R.id.btnTestConnection3)
tvTestStatus3 = findViewById(R.id.tvTestStatus3) tvTestStatus3 = findViewById(R.id.tvTestStatus3)
promptContainer = findViewById(R.id.promptContainer)
btnAddPrompt = findViewById(R.id.btnAddPrompt)
rgTheme = findViewById(R.id.rgTheme)
rbThemeLight = findViewById(R.id.rbThemeLight)
rbThemeDark = findViewById(R.id.rbThemeDark)
rbThemeAuto = findViewById(R.id.rbThemeAuto)
// 设置当前主题
when (ThemeManager.getThemeMode(this)) {
ThemeManager.THEME_LIGHT -> rbThemeLight.isChecked = true
ThemeManager.THEME_DARK -> rbThemeDark.isChecked = true
else -> rbThemeAuto.isChecked = true
}
// 主题切换监听
rgTheme.setOnCheckedChangeListener { _, checkedId ->
val mode = when (checkedId) {
R.id.rbThemeLight -> ThemeManager.THEME_LIGHT
R.id.rbThemeDark -> ThemeManager.THEME_DARK
else -> ThemeManager.THEME_FOLLOW_SYSTEM
}
ThemeManager.setThemeMode(this, mode)
}
btnAddPrompt.setOnClickListener { showAddPromptDialog() }
btnToggleApiKey1.setOnClickListener { toggleApiKeyVisibility(etApiKey1, btnToggleApiKey1) } btnToggleApiKey1.setOnClickListener { toggleApiKeyVisibility(etApiKey1, btnToggleApiKey1) }
btnToggleApiKey2.setOnClickListener { toggleApiKeyVisibility(etApiKey2, btnToggleApiKey2) } btnToggleApiKey2.setOnClickListener { toggleApiKeyVisibility(etApiKey2, btnToggleApiKey2) }
btnToggleApiKey3.setOnClickListener { toggleApiKeyVisibility(etApiKey3, btnToggleApiKey3) } btnToggleApiKey3.setOnClickListener { toggleApiKeyVisibility(etApiKey3, btnToggleApiKey3) }
@@ -106,31 +156,40 @@ class SecondActivity : AppCompatActivity() {
try { try {
val settings = Gson().fromJson(json, SettingsData::class.java) val settings = Gson().fromJson(json, SettingsData::class.java)
llmConfigs = settings.llmConfigs?.toMutableList() ?: mutableListOf() llmConfigs = settings.llmConfigs?.toMutableList() ?: mutableListOf()
promptConfigs = settings.promptConfigs?.toMutableList() ?: mutableListOf()
selectedLlmIndex = settings.selectedLlmIndex ?: 0 selectedLlmIndex = settings.selectedLlmIndex ?: 0
if (llmConfigs.isEmpty()) { if (llmConfigs.isEmpty()) {
val legacyBaseUrl = settings.llmConfig?.baseUrl ?: "https://api.openai.com/v1"
val legacyApiKey = settings.llmConfig?.apiKey ?: ""
val legacyModel = settings.llmConfig?.model ?: "gpt-4o"
llmConfigs.add(LLMConfig( llmConfigs.add(LLMConfig(
name = "默认配置", name = "模型1",
baseUrl = legacyBaseUrl, baseUrl = "https://api.openai.com/v1",
apiKey = legacyApiKey, apiKey = "",
model = legacyModel model = "gpt-4o",
enabled = true
)) ))
} }
while (llmConfigs.size < 3) { while (llmConfigs.size < 3) {
llmConfigs.add(LLMConfig( llmConfigs.add(LLMConfig(
name = "配置 ${llmConfigs.size + 1}", name = "模型${llmConfigs.size + 1}",
baseUrl = "https://api.openai.com/v1", baseUrl = "https://api.openai.com/v1",
apiKey = "", apiKey = "",
model = "gpt-4o" model = "gpt-4o",
enabled = false
)) ))
} }
// 加载默认提示词
if (promptConfigs.isEmpty()) {
promptConfigs.add(PromptConfig("default-1", "翻译助手", "将输入的文本翻译成指定语言"))
promptConfigs.add(PromptConfig("default-2", "代码解释", "解释代码的功能和逻辑"))
promptConfigs.add(PromptConfig("quick-1", "检查错别字", "请检查以下文本中的错别字并纠正:"))
promptConfigs.add(PromptConfig("quick-2", "总结", "请用简洁的语言总结以下文本的主要内容:"))
promptConfigs.add(PromptConfig("quick-3", "翻译", "请翻译以下文本:"))
promptConfigs.add(PromptConfig("quick-4", "润色", "请润色以下文本,使其更通顺流畅:"))
}
loadConfigsToViews() loadConfigsToViews()
updateApiKeyVisibilityForAll()
} catch (e: Exception) { } catch (e: Exception) {
setDefaultConfigs() setDefaultConfigs()
} }
@@ -139,58 +198,97 @@ class SecondActivity : AppCompatActivity() {
} }
} }
private fun updatePromptList() {
promptContainer.removeAllViews()
for (i in promptConfigs.indices) {
val prompt = promptConfigs[i]
val itemView = LayoutInflater.from(this).inflate(R.layout.item_prompt_config, promptContainer, false)
val tvTitle = itemView.findViewById<TextView>(R.id.tvPromptTitle)
val tvContent = itemView.findViewById<TextView>(R.id.tvPromptContent)
val btnDelete = itemView.findViewById<Button>(R.id.btnDeletePrompt)
tvTitle.text = prompt.title
tvContent.text = prompt.content
val index = i
btnDelete.setOnClickListener {
promptConfigs.removeAt(index)
updatePromptList()
}
promptContainer.addView(itemView)
}
}
private fun showAddPromptDialog() {
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_add_prompt, null)
val etTitle = dialogView.findViewById<EditText>(R.id.etPromptTitle)
val etContent = dialogView.findViewById<EditText>(R.id.etPromptContent)
AlertDialog.Builder(this)
.setTitle("添加提示词")
.setView(dialogView)
.setPositiveButton("添加") { _, _ ->
val title = etTitle.text.toString().trim()
val content = etContent.text.toString().trim()
if (title.isNotEmpty() && content.isNotEmpty()) {
promptConfigs.add(PromptConfig(
id = "custom-${promptConfigs.size}",
title = title,
content = content
))
updatePromptList()
}
}
.setNegativeButton("取消", null)
.show()
}
private fun loadConfigsToViews() { private fun loadConfigsToViews() {
if (llmConfigs.size > 0) { if (llmConfigs.size > 0) {
val config1 = llmConfigs[0] val config1 = llmConfigs[0]
cbModel1Enabled.isChecked = config1.enabled
etModelName1.setText(config1.name)
etBaseUrl1.setText(config1.baseUrl) etBaseUrl1.setText(config1.baseUrl)
etApiKey1.setText(config1.apiKey) etApiKey1.setText(config1.apiKey)
etModel1.setText(config1.model) etModel1.setText(config1.model)
etModelName1.setText(config1.name)
} }
if (llmConfigs.size > 1) { if (llmConfigs.size > 1) {
val config2 = llmConfigs[1] val config2 = llmConfigs[1]
cbModel2Enabled.isChecked = config2.enabled
etModelName2.setText(config2.name)
etBaseUrl2.setText(config2.baseUrl) etBaseUrl2.setText(config2.baseUrl)
etApiKey2.setText(config2.apiKey) etApiKey2.setText(config2.apiKey)
etModel2.setText(config2.model) etModel2.setText(config2.model)
etModelName2.setText(config2.name)
} }
if (llmConfigs.size > 2) { if (llmConfigs.size > 2) {
val config3 = llmConfigs[2] val config3 = llmConfigs[2]
cbModel3Enabled.isChecked = config3.enabled
etModelName3.setText(config3.name)
etBaseUrl3.setText(config3.baseUrl) etBaseUrl3.setText(config3.baseUrl)
etApiKey3.setText(config3.apiKey) etApiKey3.setText(config3.apiKey)
etModel3.setText(config3.model) etModel3.setText(config3.model)
etModelName3.setText(config3.name)
} }
} }
private fun setDefaultConfigs() { private fun setDefaultConfigs() {
cbModel1Enabled.isChecked = true
etModelName1.setText("模型1")
etBaseUrl1.setText("https://api.openai.com/v1") etBaseUrl1.setText("https://api.openai.com/v1")
etModel1.setText("gpt-4o") etModel1.setText("gpt-4o")
etModelName1.setText("默认配置")
cbModel2Enabled.isChecked = false
etModelName2.setText("模型2")
etBaseUrl2.setText("https://api.openai.com/v1") etBaseUrl2.setText("https://api.openai.com/v1")
etModel2.setText("gpt-4o") etModel2.setText("gpt-4o")
etModelName2.setText("配置2")
cbModel3Enabled.isChecked = false
etModelName3.setText("模型3")
etBaseUrl3.setText("https://api.openai.com/v1") etBaseUrl3.setText("https://api.openai.com/v1")
etModel3.setText("gpt-4o") etModel3.setText("gpt-4o")
etModelName3.setText("配置3")
updateApiKeyVisibilityForAll()
}
private fun updateApiKeyVisibilityForAll() {
updateApiKeyVisibility(etApiKey1, btnToggleApiKey1)
updateApiKeyVisibility(etApiKey2, btnToggleApiKey2)
updateApiKeyVisibility(etApiKey3, btnToggleApiKey3)
}
private fun updateApiKeyVisibility(editText: EditText, button: ImageButton) {
val isEmpty = editText.text.toString().isEmpty()
editText.transformationMethod = if (isEmpty) null else PasswordTransformationMethod()
editText.setSelection(editText.text.length)
} }
private fun toggleApiKeyVisibility(editText: EditText, button: ImageButton) { private fun toggleApiKeyVisibility(editText: EditText, button: ImageButton) {
@@ -314,29 +412,32 @@ class SecondActivity : AppCompatActivity() {
private fun saveConfigurations() { private fun saveConfigurations() {
llmConfigs.clear() llmConfigs.clear()
llmConfigs.add(LLMConfig( llmConfigs.add(LLMConfig(
name = etModelName1.text.toString().ifEmpty { "默认配置" }, name = etModelName1.text.toString().ifEmpty { "模型1" },
baseUrl = etBaseUrl1.text.toString(), baseUrl = etBaseUrl1.text.toString(),
apiKey = etApiKey1.text.toString(), apiKey = etApiKey1.text.toString(),
model = etModel1.text.toString() model = etModel1.text.toString(),
enabled = cbModel1Enabled.isChecked
)) ))
llmConfigs.add(LLMConfig( llmConfigs.add(LLMConfig(
name = etModelName2.text.toString().ifEmpty { "配置2" }, name = etModelName2.text.toString().ifEmpty { "模型2" },
baseUrl = etBaseUrl2.text.toString(), baseUrl = etBaseUrl2.text.toString(),
apiKey = etApiKey2.text.toString(), apiKey = etApiKey2.text.toString(),
model = etModel2.text.toString() model = etModel2.text.toString(),
enabled = cbModel2Enabled.isChecked
)) ))
llmConfigs.add(LLMConfig( llmConfigs.add(LLMConfig(
name = etModelName3.text.toString().ifEmpty { "配置3" }, name = etModelName3.text.toString().ifEmpty { "模型3" },
baseUrl = etBaseUrl3.text.toString(), baseUrl = etBaseUrl3.text.toString(),
apiKey = etApiKey3.text.toString(), apiKey = etApiKey3.text.toString(),
model = etModel3.text.toString() model = etModel3.text.toString(),
enabled = cbModel3Enabled.isChecked
)) ))
val settingsData = SettingsData( val settingsData = SettingsData(
llmConfigs = llmConfigs, llmConfigs = llmConfigs,
selectedLlmIndex = selectedLlmIndex, selectedLlmIndex = selectedLlmIndex,
headerConfigs = null, headerConfigs = null,
promptConfigs = null, promptConfigs = promptConfigs,
buttonConfigs = null, buttonConfigs = null,
noteApiConfig = null noteApiConfig = null
) )
@@ -354,6 +455,8 @@ class SecondActivity : AppCompatActivity() {
val enabled: Boolean = true val enabled: Boolean = true
) )
data class PromptConfig(val id: String, val title: String, val content: String)
data class SettingsData( data class SettingsData(
val llmConfigs: List<LLMConfig>?, val llmConfigs: List<LLMConfig>?,
val selectedLlmIndex: Int?, val selectedLlmIndex: Int?,
@@ -365,7 +468,6 @@ class SecondActivity : AppCompatActivity() {
) )
data class HeaderConfig(val key: String, val value: String) 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) 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)
data class NoteApiConfig(val apiType: String, val apiUrl: String, val apiKey: String) data class NoteApiConfig(val apiType: String, val apiUrl: String, val apiKey: String)
} }

View File

@@ -69,7 +69,7 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="20dp"> android:padding="20dp">
<!-- 提示词选择区:左侧标签+下拉框,右侧快捷按钮 --> <!-- 提示词选择区 -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -175,7 +175,7 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<!-- 提示词详情:显示名称和内容 --> <!-- 提示词详情 -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -212,162 +212,236 @@
android:text="无特殊指令"/> android:text="无特殊指令"/>
</LinearLayout> </LinearLayout>
<!-- 大模型返回结果 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="大模型返回结果"
android:textSize="12sp"
android:textColor="@color/text_hint"
android:textAllCaps="true"
android:letterSpacing="0.15"
android:layout_marginBottom="14dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/result_card_bg"
android:padding="16dp">
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/primary"
android:layout_marginBottom="12dp"/>
<TextView
android:id="@+id/outputStatusLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="等待发送"
android:textSize="12sp"
android:textColor="@color/primary"
android:layout_marginBottom="8dp"/>
<EditText
android:id="@+id/outputTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="3"
android:textSize="14sp"
android:text="发送消息后结果将在此显示"
android:textColor="@color/text_secondary"
android:editable="true"
android:gravity="top"
android:background="@android:drawable/edit_text"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="12dp">
<Button
android:id="@+id/btnCopyResult"
android:layout_width="0dp"
android:layout_height="32dp"
android:layout_weight="1"
android:text="复制结果"
android:textSize="12sp"
android:textColor="@color/primary"
android:background="@drawable/button_secondary_bg"
android:minWidth="0dp"
android:minHeight="0dp"
android:layout_marginEnd="8dp"/>
<Button
android:id="@+id/btnSaveNote"
android:layout_width="0dp"
android:layout_height="32dp"
android:layout_weight="1"
android:text="提交笔记"
android:textSize="12sp"
android:textColor="@color/primary"
android:background="@drawable/button_secondary_bg"
android:minWidth="0dp"
android:minHeight="0dp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- 输入区域 --> <!-- 输入区域 -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:background="@color/surface" android:background="@drawable/input_bg"
android:padding="20dp"> android:padding="12dp"
android:layout_marginBottom="14dp">
<EditText <EditText
android:id="@+id/inputEditText" android:id="@+id/inputEditText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minLines="3" android:minLines="2"
android:maxLines="10" android:maxLines="5"
android:background="@drawable/input_bg"
android:hint="输入待发送内容…" android:hint="输入待发送内容…"
android:inputType="textCapSentences|textMultiLine" android:inputType="textCapSentences|textMultiLine"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/text_secondary" android:textColor="@color/text_secondary"
android:textColorHint="@color/text_hint" android:textColorHint="@color/text_hint"
android:gravity="top|start" android:gravity="top|start"
android:scrollbars="vertical"/> android:background="@android:color/transparent"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginTop="12dp" android:layout_marginTop="8dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<Button
android:id="@+id/stopButton"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="停止生成"
android:textSize="12sp"
android:textColor="@color/stop_generate"
android:background="@drawable/stop_button_bg"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:visibility="gone"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView <TextView
android:id="@+id/tvCharCount" android:id="@+id/tvCharCount"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:text="0/4000" android:text="0/4000"
android:textSize="11sp" android:textSize="11sp"
android:textColor="@color/text_hint" android:textColor="@color/text_hint"/>
android:layout_marginEnd="12dp"/>
<Button <Button
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="42dp" android:layout_width="wrap_content"
android:layout_height="42dp" android:layout_height="32dp"
android:text="" android:text="发送"
android:textSize="18sp" android:textSize="13sp"
android:textColor="@color/white" android:textColor="@color/white"
android:background="@drawable/send_button_bg" android:background="@drawable/send_button_bg"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:minWidth="0dp" android:minWidth="0dp"
android:minHeight="0dp"/> android:minHeight="0dp"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- 三栏结果显示 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="各模型返回结果"
android:textSize="12sp"
android:textColor="@color/text_hint"
android:textAllCaps="true"
android:letterSpacing="0.15"
android:layout_marginBottom="10dp"/>
<!-- 模型1 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/result_card_bg"
android:padding="12dp"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvModel1Name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="● 模型1"
android:textSize="13sp"
android:textColor="@color/primary"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvStatus1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="等待..."
android:textSize="11sp"
android:textColor="@color/text_hint"/>
</LinearLayout> </LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"/>
<TextView
android:id="@+id/tvResult1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="等待结果..."
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:minLines="3"/>
</LinearLayout>
<!-- 模型2 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/result_card_bg"
android:padding="12dp"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvModel2Name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="○ 模型2"
android:textSize="13sp"
android:textColor="@color/text_hint"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvStatus2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="等待..."
android:textSize="11sp"
android:textColor="@color/text_hint"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"/>
<TextView
android:id="@+id/tvResult2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="等待结果..."
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:minLines="3"/>
</LinearLayout>
<!-- 模型3 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/result_card_bg"
android:padding="12dp"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvModel3Name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="○ 模型3"
android:textSize="13sp"
android:textColor="@color/text_hint"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvStatus3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="等待..."
android:textSize="11sp"
android:textColor="@color/text_hint"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"/>
<TextView
android:id="@+id/tvResult3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="等待结果..."
android:textSize="13sp"
android:textColor="@color/text_secondary"
android:minLines="3"/>
</LinearLayout>
<!-- 复制结果按钮 -->
<Button
android:id="@+id/btnCopyResult"
android:layout_width="match_parent"
android:layout_height="36dp"
android:text="复制所有结果"
android:textSize="13sp"
android:textColor="@color/primary"
android:background="@drawable/button_secondary_bg"
android:layout_marginTop="8dp"/>
</LinearLayout>
</ScrollView>
</LinearLayout> </LinearLayout>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
<?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="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题"
android:textSize="12sp"
android:textColor="@color/text_hint"/>
<EditText
android:id="@+id/etPromptTitle"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="例如:翻译助手"
android:textSize="14sp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:background="@drawable/edittext_border"
android:layout_marginTop="4dp"
android:layout_marginBottom="12dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="内容"
android:textSize="12sp"
android:textColor="@color/text_hint"/>
<EditText
android:id="@+id/etPromptContent"
android:layout_width="match_parent"
android:layout_height="80dp"
android:hint="提示词内容..."
android:textSize="14sp"
android:padding="10dp"
android:background="@drawable/edittext_border"
android:layout_marginTop="4dp"
android:gravity="top"
android:inputType="textMultiLine"/>
</LinearLayout>

View File

@@ -0,0 +1,46 @@
<?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:background="@drawable/edittext_border"
android:padding="12dp"
android:layout_marginBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvPromptTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:textColor="@color/text_primary"
android:textStyle="bold"/>
<Button
android:id="@+id/btnDeletePrompt"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:text="删除"
android:textSize="12sp"
android:textColor="@color/error"
android:background="@drawable/button_secondary_bg"
android:minHeight="0dp"/>
</LinearLayout>
<TextView
android:id="@+id/tvPromptContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="12sp"
android:textColor="@color/text_hint"
android:maxLines="2"
android:ellipsize="end"/>
</LinearLayout>