Compare commits

...

3 Commits

8 changed files with 1162 additions and 895 deletions

View File

@@ -22,3 +22,6 @@ lto = true
codegen-units = 1
panic = "abort"
strip = true
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "link-args=-Wl,--subsystem,windows"]

View File

@@ -34,6 +34,7 @@ pub struct FlomoAiApp {
model_displays: Vec<ModelDisplay>,
pending_results: Option<std::thread::JoinHandle<Vec<ModelResult>>>,
pending_tests: Vec<(usize, std::thread::JoinHandle<Result<String, String>>)>,
settings_selected_theme: ThemeMode,
new_prompt_title: String,
@@ -65,6 +66,7 @@ impl FlomoAiApp {
char_count: 0,
model_displays,
pending_results: None,
pending_tests: Vec::new(),
settings_selected_theme: ThemeMode::Light,
new_prompt_title: String::new(),
new_prompt_content: String::new(),
@@ -73,6 +75,28 @@ impl FlomoAiApp {
}
}
fn poll_test_results(&mut self, ctx: &egui::Context) {
for (index, handle) in self.pending_tests.drain(..) {
match handle.join() {
Ok(Ok(response)) => {
let msg = if response.len() > 30 {
format!("成功: {}...", &response[..30])
} else {
format!("成功: {}", response)
};
self.model_displays[index].test_status = msg;
}
Ok(Err(e)) => {
self.model_displays[index].test_status = format!("失败: {}", e);
}
Err(_) => {
self.model_displays[index].test_status = "测试线程错误".to_string();
}
}
ctx.request_repaint();
}
}
fn poll_results(&mut self, ctx: &egui::Context) {
if let Some(handle) = self.pending_results.take() {
if handle.is_finished() {
@@ -184,6 +208,7 @@ impl FlomoAiApp {
impl eframe::App for FlomoAiApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.poll_results(ctx);
self.poll_test_results(ctx);
if self.theme_dirty {
let theme = AppTheme::from_mode(self.settings.theme_config.mode);
@@ -287,51 +312,49 @@ impl FlomoAiApp {
ui.label(egui::RichText::new("各模型返回结果").size(11.0).color(egui::Color32::GRAY));
ui.add_space(6.0);
let available_width = ui.available_width();
let column_width = (available_width - 16.0) / 3.0;
let total_width = ui.available_width();
let spacing = 8.0;
let column_width = (total_width - spacing * 2.0) / 3.0;
ui.horizontal(|ui| {
ui.set_height(200.0);
for (i, display) in self.model_displays.iter().enumerate() {
if i > 0 {
ui.add_space(8.0);
}
ui.set_min_width(column_width);
ui.horizontal_wrapped(|ui| {
ui.set_width(total_width);
for d in self.model_displays.iter() {
egui::Frame::none()
.fill(ui.style().visuals.widgets.inactive.bg_fill)
.fill(if d.enabled {
ui.style().visuals.widgets.inactive.bg_fill
} else {
egui::Color32::from_rgb(235, 235, 240)
})
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(180, 180, 200)))
.rounding(8.0)
.inner_margin(egui::Margin::same(10.0))
.inner_margin(egui::Margin::same(8.0))
.show(ui, |ui| {
ui.set_min_width(column_width - 20.0);
ui.set_min_width(column_width - 16.0);
let enabled_color = if display.enabled {
let enabled_color = if d.enabled {
egui::Color32::from_rgb(80, 80, 220)
} else {
egui::Color32::GRAY
};
let enabled_dot = if display.enabled { "" } else { "" };
ui.label(egui::RichText::new(format!("{} {}", enabled_dot, display.name))
let enabled_dot = if d.enabled { "" } else { "" };
ui.label(egui::RichText::new(format!("{} {}", enabled_dot, d.name))
.size(13.0).strong().color(enabled_color));
ui.label(egui::RichText::new(&display.model).size(10.0).color(egui::Color32::GRAY));
ui.label(egui::RichText::new(&d.model).size(10.0).color(egui::Color32::GRAY));
ui.add_space(6.0);
ui.add_space(4.0);
ui.add_sized([ui.available_width(), 1.0], egui::Separator::default());
ui.add_space(6.0);
let status_color = match &display.status {
let status_color = match &d.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 {
ui.label(egui::RichText::new(match &d.status {
ModelStatus::Waiting => "● 就绪",
ModelStatus::Loading => "◐ 生成中...",
ModelStatus::Completed => "✓ 完成",
@@ -340,26 +363,26 @@ impl FlomoAiApp {
ui.add_space(4.0);
if display.result.is_empty() {
if d.result.is_empty() {
ui.label(egui::RichText::new("等待结果...").size(11.0).color(egui::Color32::GRAY));
} else {
let mut result_text = display.result.clone();
let mut result_text = d.result.clone();
ui.add_sized(
[ui.available_width(), 120.0],
[ui.available_width(), 100.0],
egui::TextEdit::multiline(&mut result_text)
.desired_rows(5)
.desired_rows(4)
.frame(false),
);
}
ui.add_space(6.0);
ui.add_space(4.0);
ui.horizontal(|ui| {
if ui.small_button("复制").clicked() && !display.result.is_empty() {
self.copy_to_clipboard(&display.result);
if ui.small_button("复制").clicked() && !d.result.is_empty() {
self.copy_to_clipboard(&d.result);
}
});
});
ui.add_space(spacing);
}
});
});
@@ -380,20 +403,20 @@ impl FlomoAiApp {
ScrollArea::vertical().show(ui, |ui| {
for i in 0..self.model_displays.len() {
let enabled = self.model_displays[i].enabled;
let name = self.model_displays[i].name.clone();
let model = self.model_displays[i].model.clone();
ui.add_space(8.0);
ui.horizontal(|ui| {
self.model_displays[i].enabled ^= ui.checkbox(&mut self.model_displays[i].enabled, "").changed();
ui.label(egui::RichText::new(&format!("模型 {} 配置", i + 1)).size(14.0).strong());
ui.checkbox(&mut self.model_displays[i].enabled, "");
let label = if self.model_displays[i].enabled {
format!("{} ({})", self.model_displays[i].name, self.model_displays[i].model)
} else {
format!("{} (已禁用)", self.model_displays[i].name)
};
ui.label(egui::RichText::new(&label).size(14.0).strong());
});
if !enabled {
ui.label(egui::RichText::new("已禁用").size(11.0).color(egui::Color32::GRAY));
} else {
ui.add_space(4.0);
ui.label("名称");
ui.text_edit_singleline(&mut self.model_displays[i].name);
@@ -423,6 +446,9 @@ impl FlomoAiApp {
ui.label("Model");
ui.text_edit_singleline(&mut self.model_displays[i].model);
if i < self.settings.llm_configs.models.len() {
self.settings.llm_configs.models[i].model = self.model_displays[i].model.clone();
}
ui.add_space(6.0);
@@ -432,22 +458,21 @@ impl FlomoAiApp {
.rounding(4.0)
.min_size(egui::vec2(60.0, 28.0));
ui.horizontal(|ui| {
if ui.add(test_btn).clicked() {
if let Some(config) = self.settings.llm_configs.models.get(i).cloned() {
self.model_displays[i].test_status = "测试中...".to_string();
let headers = self.settings.header_configs.clone();
let ctx_clone = ctx.clone();
let display_index = i;
std::thread::spawn(move || {
let result = test_single_llm(&config, &headers);
ctx_clone.request_repaint();
let handle = std::thread::spawn(move || {
test_single_llm(&config, &headers)
});
self.pending_tests.push((i, handle));
}
}
ui.label(egui::RichText::new(&test_status).size(11.0).color(egui::Color32::GRAY));
}
});
if i < self.model_displays.len() - 1 {
ui.separator();

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.ArrayAdapter
@@ -31,13 +32,19 @@ import org.json.JSONObject
class MainActivity : AppCompatActivity() {
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 promptNameText: TextView
private lateinit var promptContentText: TextView
private var llmConfigs = listOf<LLMConfig>()
private var selectedLlmIndex = 0
private val results = mutableMapOf<Int, String>()
@SuppressLint("MissingInflatedId", "CutPasteId", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -51,6 +58,30 @@ class MainActivity : AppCompatActivity() {
inputEditText = findViewById<EditText>(R.id.inputEditText)
val sendButton = findViewById<Button>(R.id.sendButton)
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()
loadPromptsFromConfig()
@@ -104,8 +135,13 @@ class MainActivity : AppCompatActivity() {
}
btnCopyResult.setOnClickListener {
val textToCopy = outputTextView.text.toString()
if (textToCopy.isNotEmpty() && textToCopy != "发送消息后结果将在此显示") {
val sb = StringBuilder()
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 clip = ClipData.newPlainText("优化结果", textToCopy)
clipboard.setPrimaryClip(clip)
@@ -118,6 +154,11 @@ class MainActivity : AppCompatActivity() {
initQuickButtons()
}
override fun onResume() {
super.onResume()
loadModelsFromConfig()
}
private fun loadModelsFromConfig() {
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null)
@@ -131,6 +172,19 @@ class MainActivity : AppCompatActivity() {
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) {
@@ -143,22 +197,49 @@ class MainActivity : AppCompatActivity() {
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 apiKey = config.apiKey
val model = config.model.ifEmpty { "gpt-4o" }
// 找出所有启用的模型
val enabledConfigs = configs.filter { it.enabled && it.baseUrl.isNotEmpty() && it.model.isNotEmpty() }
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) {
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) {
val outputStatusLabel = findViewById<TextView>(R.id.outputStatusLabel)
outputStatusLabel.text = "连接中..."
outputTextView.setText("正在生成...")
private fun sendToLlm(baseUrl: String, apiKey: String, model: String, content: String, index: Int, total: Int) {
val statusView = when (index) {
0 -> tvStatus1
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 {
try {
@@ -198,21 +279,22 @@ class MainActivity : AppCompatActivity() {
if (choices.length() > 0) {
val message = choices.getJSONObject(0).getJSONObject("message")
val result = message.getString("content")
outputStatusLabel.text = "完成"
outputTextView.setText(result)
statusView.text = "完成"
resultView.text = result
results[index] = result
} else {
outputStatusLabel.text = "发生错误"
outputTextView.setText("API 返回空结果")
statusView.text = "错误"
resultView.text = "API 返回空结果"
}
} else {
outputStatusLabel.text = "发生错误"
outputTextView.setText("API 错误: ${response.code} ${response.message}")
statusView.text = "错误"
resultView.text = "API 错误: ${response.code} ${response.message}"
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
outputStatusLabel.text = "发生错误"
outputTextView.setText("错误: ${e.message}")
statusView.text = "错误"
resultView.text = "错误: ${e.message}"
}
}
}

View File

@@ -2,13 +2,22 @@ package com.example.flomo_ai
import android.content.Context
import android.os.Bundle
import android.text.InputType
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.CheckBox
import android.widget.EditText
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.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.flomo_ai.ui.theme.ThemeManager
@@ -24,31 +33,42 @@ import okhttp3.RequestBody.Companion.toRequestBody
class SecondActivity : AppCompatActivity() {
private lateinit var cbModel1Enabled: CheckBox
private lateinit var etModelName1: EditText
private lateinit var etBaseUrl1: EditText
private lateinit var etApiKey1: EditText
private lateinit var btnToggleApiKey1: ImageButton
private lateinit var etModel1: EditText
private lateinit var etModelName1: EditText
private lateinit var btnTestConnection1: Button
private lateinit var tvTestStatus1: TextView
private lateinit var cbModel2Enabled: CheckBox
private lateinit var etModelName2: EditText
private lateinit var etBaseUrl2: EditText
private lateinit var etApiKey2: EditText
private lateinit var btnToggleApiKey2: ImageButton
private lateinit var etModel2: EditText
private lateinit var etModelName2: EditText
private lateinit var btnTestConnection2: Button
private lateinit var tvTestStatus2: TextView
private lateinit var cbModel3Enabled: CheckBox
private lateinit var etModelName3: EditText
private lateinit var etBaseUrl3: EditText
private lateinit var etApiKey3: EditText
private lateinit var btnToggleApiKey3: ImageButton
private lateinit var etModel3: EditText
private lateinit var etModelName3: EditText
private lateinit var btnTestConnection3: Button
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 promptConfigs = mutableListOf<PromptConfig>()
private var selectedLlmIndex = 0
override fun onCreate(savedInstanceState: Bundle?) {
@@ -58,6 +78,7 @@ class SecondActivity : AppCompatActivity() {
initViews()
loadConfigurations()
updatePromptList()
findViewById<ImageButton>(R.id.btnBack).setOnClickListener {
finish()
@@ -65,30 +86,59 @@ class SecondActivity : AppCompatActivity() {
}
private fun initViews() {
cbModel1Enabled = findViewById(R.id.cbModel1Enabled)
etModelName1 = findViewById(R.id.etModelName1)
etBaseUrl1 = findViewById(R.id.etBaseUrl1)
etApiKey1 = findViewById(R.id.etApiKey1)
btnToggleApiKey1 = findViewById(R.id.btnToggleApiKey1)
etModel1 = findViewById(R.id.etModel1)
etModelName1 = findViewById(R.id.etModelName1)
btnTestConnection1 = findViewById(R.id.btnTestConnection1)
tvTestStatus1 = findViewById(R.id.tvTestStatus1)
cbModel2Enabled = findViewById(R.id.cbModel2Enabled)
etModelName2 = findViewById(R.id.etModelName2)
etBaseUrl2 = findViewById(R.id.etBaseUrl2)
etApiKey2 = findViewById(R.id.etApiKey2)
btnToggleApiKey2 = findViewById(R.id.btnToggleApiKey2)
etModel2 = findViewById(R.id.etModel2)
etModelName2 = findViewById(R.id.etModelName2)
btnTestConnection2 = findViewById(R.id.btnTestConnection2)
tvTestStatus2 = findViewById(R.id.tvTestStatus2)
cbModel3Enabled = findViewById(R.id.cbModel3Enabled)
etModelName3 = findViewById(R.id.etModelName3)
etBaseUrl3 = findViewById(R.id.etBaseUrl3)
etApiKey3 = findViewById(R.id.etApiKey3)
btnToggleApiKey3 = findViewById(R.id.btnToggleApiKey3)
etModel3 = findViewById(R.id.etModel3)
etModelName3 = findViewById(R.id.etModelName3)
btnTestConnection3 = findViewById(R.id.btnTestConnection3)
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) }
btnToggleApiKey2.setOnClickListener { toggleApiKeyVisibility(etApiKey2, btnToggleApiKey2) }
btnToggleApiKey3.setOnClickListener { toggleApiKeyVisibility(etApiKey3, btnToggleApiKey3) }
@@ -106,31 +156,40 @@ class SecondActivity : AppCompatActivity() {
try {
val settings = Gson().fromJson(json, SettingsData::class.java)
llmConfigs = settings.llmConfigs?.toMutableList() ?: mutableListOf()
promptConfigs = settings.promptConfigs?.toMutableList() ?: mutableListOf()
selectedLlmIndex = settings.selectedLlmIndex ?: 0
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(
name = "默认配置",
baseUrl = legacyBaseUrl,
apiKey = legacyApiKey,
model = legacyModel
name = "模型1",
baseUrl = "https://api.openai.com/v1",
apiKey = "",
model = "gpt-4o",
enabled = true
))
}
while (llmConfigs.size < 3) {
llmConfigs.add(LLMConfig(
name = "配置 ${llmConfigs.size + 1}",
name = "模型${llmConfigs.size + 1}",
baseUrl = "https://api.openai.com/v1",
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()
updateApiKeyVisibilityForAll()
} catch (e: Exception) {
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() {
if (llmConfigs.size > 0) {
val config1 = llmConfigs[0]
cbModel1Enabled.isChecked = config1.enabled
etModelName1.setText(config1.name)
etBaseUrl1.setText(config1.baseUrl)
etApiKey1.setText(config1.apiKey)
etModel1.setText(config1.model)
etModelName1.setText(config1.name)
}
if (llmConfigs.size > 1) {
val config2 = llmConfigs[1]
cbModel2Enabled.isChecked = config2.enabled
etModelName2.setText(config2.name)
etBaseUrl2.setText(config2.baseUrl)
etApiKey2.setText(config2.apiKey)
etModel2.setText(config2.model)
etModelName2.setText(config2.name)
}
if (llmConfigs.size > 2) {
val config3 = llmConfigs[2]
cbModel3Enabled.isChecked = config3.enabled
etModelName3.setText(config3.name)
etBaseUrl3.setText(config3.baseUrl)
etApiKey3.setText(config3.apiKey)
etModel3.setText(config3.model)
etModelName3.setText(config3.name)
}
}
private fun setDefaultConfigs() {
cbModel1Enabled.isChecked = true
etModelName1.setText("模型1")
etBaseUrl1.setText("https://api.openai.com/v1")
etModel1.setText("gpt-4o")
etModelName1.setText("默认配置")
cbModel2Enabled.isChecked = false
etModelName2.setText("模型2")
etBaseUrl2.setText("https://api.openai.com/v1")
etModel2.setText("gpt-4o")
etModelName2.setText("配置2")
cbModel3Enabled.isChecked = false
etModelName3.setText("模型3")
etBaseUrl3.setText("https://api.openai.com/v1")
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) {
@@ -314,29 +412,32 @@ class SecondActivity : AppCompatActivity() {
private fun saveConfigurations() {
llmConfigs.clear()
llmConfigs.add(LLMConfig(
name = etModelName1.text.toString().ifEmpty { "默认配置" },
name = etModelName1.text.toString().ifEmpty { "模型1" },
baseUrl = etBaseUrl1.text.toString(),
apiKey = etApiKey1.text.toString(),
model = etModel1.text.toString()
model = etModel1.text.toString(),
enabled = cbModel1Enabled.isChecked
))
llmConfigs.add(LLMConfig(
name = etModelName2.text.toString().ifEmpty { "配置2" },
name = etModelName2.text.toString().ifEmpty { "模型2" },
baseUrl = etBaseUrl2.text.toString(),
apiKey = etApiKey2.text.toString(),
model = etModel2.text.toString()
model = etModel2.text.toString(),
enabled = cbModel2Enabled.isChecked
))
llmConfigs.add(LLMConfig(
name = etModelName3.text.toString().ifEmpty { "配置3" },
name = etModelName3.text.toString().ifEmpty { "模型3" },
baseUrl = etBaseUrl3.text.toString(),
apiKey = etApiKey3.text.toString(),
model = etModel3.text.toString()
model = etModel3.text.toString(),
enabled = cbModel3Enabled.isChecked
))
val settingsData = SettingsData(
llmConfigs = llmConfigs,
selectedLlmIndex = selectedLlmIndex,
headerConfigs = null,
promptConfigs = null,
promptConfigs = promptConfigs,
buttonConfigs = null,
noteApiConfig = null
)
@@ -354,6 +455,8 @@ class SecondActivity : AppCompatActivity() {
val enabled: Boolean = true
)
data class PromptConfig(val id: String, val title: String, val content: String)
data class SettingsData(
val llmConfigs: List<LLMConfig>?,
val selectedLlmIndex: Int?,
@@ -365,7 +468,6 @@ class SecondActivity : AppCompatActivity() {
)
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 NoteApiConfig(val apiType: String, val apiUrl: String, val apiKey: String)
}

View File

@@ -69,7 +69,7 @@
android:orientation="vertical"
android:padding="20dp">
<!-- 提示词选择区:左侧标签+下拉框,右侧快捷按钮 -->
<!-- 提示词选择区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -175,7 +175,7 @@
</LinearLayout>
</LinearLayout>
<!-- 提示词详情:显示名称和内容 -->
<!-- 提示词详情 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -212,162 +212,236 @@
android:text="无特殊指令"/>
</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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/surface"
android:padding="20dp">
android:background="@drawable/input_bg"
android:padding="12dp"
android:layout_marginBottom="14dp">
<EditText
android:id="@+id/inputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="3"
android:maxLines="10"
android:background="@drawable/input_bg"
android:minLines="2"
android:maxLines="5"
android:hint="输入待发送内容…"
android:inputType="textCapSentences|textMultiLine"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:textColorHint="@color/text_hint"
android:gravity="top|start"
android:scrollbars="vertical"/>
android:background="@android:color/transparent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="12dp"
android:layout_marginTop="8dp"
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
android:id="@+id/tvCharCount"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="0/4000"
android:textSize="11sp"
android:textColor="@color/text_hint"
android:layout_marginEnd="12dp"/>
android:textColor="@color/text_hint"/>
<Button
android:id="@+id/sendButton"
android:layout_width="42dp"
android:layout_height="42dp"
android:text=""
android:textSize="18sp"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="发送"
android:textSize="13sp"
android:textColor="@color/white"
android:background="@drawable/send_button_bg"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:minWidth="0dp"
android:minHeight="0dp"/>
</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>
<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>

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>