v1.2增加了双对话功能

This commit is contained in:
lotus 2025-05-07 19:39:10 +08:00
parent 8940aba889
commit addbabeb8a
7 changed files with 388 additions and 245 deletions

View File

@ -6,6 +6,7 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils; // Import TextUtils
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@ -23,7 +24,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.lotus_lab.jetchat.data.AppDatabase;
import com.lotus_lab.jetchat.data.ChatMessage;
import com.lotus_lab.jetchat.data.Conversation; // 假设你已创建此类
import com.lotus_lab.jetchat.data.Conversation;
import com.lotus_lab.jetchat.network.ApiClient;
import com.lotus_lab.jetchat.network.ChatRequest;
import com.lotus_lab.jetchat.network.ChatResponse;
@ -33,7 +34,7 @@ import com.lotus_lab.jetchat.ui.ChatAdapter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
// import java.util.UUID; // UUID not directly used in this version
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -44,7 +45,7 @@ import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final int MAX_CONTEXT_MESSAGES = 10;
private static final int MAX_CONTEXT_MESSAGES = 10; // Max messages to send as context
private RecyclerView recyclerViewChat;
private EditText editTextMessage;
@ -57,16 +58,17 @@ public class MainActivity extends AppCompatActivity {
private ExecutorService databaseExecutor;
private Handler mainThreadHandler;
// 用于会话管理
private long currentConversationId = -1; // -1 表示新对话或未初始化的对话
private List<ChatMessage> currentMessageListForSaving = new ArrayList<>(); // 存储当前对话的消息
private long currentConversationId = -1;
private List<ChatMessage> currentMessageListForSaving = new ArrayList<>();
private SharedPreferences sharedPreferences;
private String apiKey;
private String modelName;
// private String modelName; // Will be replaced by modelName1 and modelName2
private String modelName1; // For the primary model
private String modelName2; // For the secondary/comparison model (can be empty)
private String apiBaseUrl;
// private String currentSessionId = null; // 旧的 sessionId 逻辑将被 currentConversationId 替代大部分功能
private int activeApiCallCount = 0; // Counter for active API calls
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -74,7 +76,7 @@ public class MainActivity extends AppCompatActivity {
setContentView(R.layout.activity_main);
sharedPreferences = getSharedPreferences(SettingsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE);
loadApiSettings();
// loadApiSettings(); // Moved to onResume and before sending message for freshness
mainThreadHandler = new Handler(Looper.getMainLooper());
recyclerViewChat = findViewById(R.id.recyclerViewChat);
@ -82,18 +84,14 @@ public class MainActivity extends AppCompatActivity {
buttonSend = findViewById(R.id.buttonSend);
progressBar = findViewById(R.id.progressBar);
apiService = ApiClient.getClient(this).create(DeepSeekApiService.class);
// apiService = ApiClient.getClient(this).create(DeepSeekApiService.class); // Initialize in onResume
database = AppDatabase.getDatabase(this);
databaseExecutor = Executors.newSingleThreadExecutor();
chatAdapter = new ChatAdapter(new ArrayList<>()); // ChatAdapter 现在只显示当前会话的消息
chatAdapter = new ChatAdapter(new ArrayList<>());
recyclerViewChat.setLayoutManager(new LinearLayoutManager(this));
recyclerViewChat.setAdapter(chatAdapter);
// 旧的 sessionId 初始化可以移除或让 startNewConversationSession 负责
// currentSessionId = generateNewSessionId();
// Log.d(TAG, "Old session ID logic: " + currentSessionId);
loadChatHistory(); // 这将启动一个新的会话
loadChatHistory(); // This will start a new conversation session
buttonSend.setOnClickListener(v -> sendMessage());
}
@ -102,21 +100,28 @@ public class MainActivity extends AppCompatActivity {
protected void onResume() {
super.onResume();
String newBaseUrlSetting = sharedPreferences.getString(SettingsActivity.KEY_API_BASE_URL, SettingsActivity.DEFAULT_API_BASE_URL);
if (apiBaseUrl == null || !apiBaseUrl.equals(newBaseUrlSetting)) {
Log.d(TAG, "Base URL changed or was null. Old: " + apiBaseUrl + ", New: " + newBaseUrlSetting);
ApiClient.resetClient();
// Check if apiBaseUrl needs update or if apiService needs reinitialization
if (apiBaseUrl == null || !apiBaseUrl.equals(newBaseUrlSetting) || apiService == null) {
Log.d(TAG, "Base URL changed, was null, or apiService was null. Old: " + apiBaseUrl + ", New: " + newBaseUrlSetting);
ApiClient.resetClient(); // Reset if your client caches the base URL
// Load settings again as base URL might have changed
loadApiSettings();
apiService = ApiClient.getClient(this).create(DeepSeekApiService.class);
} else {
// Still load settings in case other things like API key or model changed
loadApiSettings();
}
loadApiSettings(); // 确保API设置是最新的
apiService = ApiClient.getClient(this).create(DeepSeekApiService.class); // 重新获取服务实例
}
private void loadApiSettings() {
apiKey = sharedPreferences.getString(SettingsActivity.KEY_API_KEY, "");
modelName = sharedPreferences.getString(SettingsActivity.KEY_SELECTED_MODEL, SettingsActivity.DEFAULT_MODEL);
apiBaseUrl = sharedPreferences.getString(SettingsActivity.KEY_API_BASE_URL, SettingsActivity.DEFAULT_API_BASE_URL);
// Load both model names
modelName1 = sharedPreferences.getString(SettingsActivity.KEY_SELECTED_MODEL_1, SettingsActivity.DEFAULT_MODEL_1);
modelName2 = sharedPreferences.getString(SettingsActivity.KEY_SELECTED_MODEL_2, SettingsActivity.DEFAULT_MODEL_2);
Log.d(TAG, "Settings loaded: API Key (ends with): ..." + (apiKey.length() > 4 ? apiKey.substring(apiKey.length() - 4) : apiKey) +
", Model=" + modelName + ", BaseURL=" + apiBaseUrl);
", Model1=" + modelName1 + ", Model2=" + modelName2 + ", BaseURL=" + apiBaseUrl);
String placeholderApiKey = "";
try {
@ -127,32 +132,38 @@ public class MainActivity extends AppCompatActivity {
if (apiKey.isEmpty() || apiKey.equals(placeholderApiKey)) {
Log.w(TAG, "API Key is not set or is placeholder.");
// Consider showing a persistent warning or guiding user to settings
}
if (TextUtils.isEmpty(modelName1) && TextUtils.isEmpty(modelName2)) {
Log.w(TAG, "Neither Model 1 nor Model 2 is configured.");
// This case should ideally be handled by SettingsActivity ensuring model1 has a default
}
}
private void refreshConversation() {
Log.d(TAG, "refreshConversation called. Current active conversationId: " + currentConversationId);
saveCurrentConversationIfNeeded();
saveCurrentConversationIfNeeded(); // Save previous before starting new
startNewConversationSession();
chatAdapter.clearMessages(); // 清空UI
currentMessageListForSaving.clear(); // 清空内存中的消息列表
chatAdapter.clearMessages();
currentMessageListForSaving.clear();
Toast.makeText(this, "新对话已开始", Toast.LENGTH_SHORT).show();
setLoadingState(false);
setLoadingState(false); // Ensure loading is off for new conversation
editTextMessage.setText("");
editTextMessage.requestFocus();
scrollToBottom(false); // 滚动到底部
scrollToBottom(false);
}
private void startNewConversationSession() {
// Creates a new conversation entry in the DB and sets currentConversationId
Conversation newSessionConversation = new Conversation(System.currentTimeMillis(), "新对话开始...", System.currentTimeMillis());
databaseExecutor.execute(() -> {
currentConversationId = database.conversationDao().insertConversation(newSessionConversation);
Log.i(TAG, "New conversation session started with ID: " + currentConversationId);
mainThreadHandler.post(() -> {
currentMessageListForSaving.clear(); // 为新会话准备
if(chatAdapter != null) chatAdapter.clearMessages(); // 确保UI也清空
currentMessageListForSaving.clear();
if (chatAdapter != null) chatAdapter.clearMessages();
});
});
}
@ -160,14 +171,15 @@ public class MainActivity extends AppCompatActivity {
private void saveCurrentConversationIfNeeded() {
if (currentConversationId != -1 && !currentMessageListForSaving.isEmpty()) {
Log.d(TAG, "Saving current conversation ID: " + currentConversationId + " with " + currentMessageListForSaving.size() + " messages.");
final List<ChatMessage> messagesToSave = new ArrayList<>(currentMessageListForSaving);
final List<ChatMessage> messagesToSave = new ArrayList<>(currentMessageListForSaving); // Create a copy
final long conversationIdToSave = currentConversationId;
databaseExecutor.execute(() -> {
if (messagesToSave.isEmpty()) return; // Should not happen if check above is done
ChatMessage lastMessage = messagesToSave.get(messagesToSave.size() - 1);
String preview = lastMessage.content;
if (preview.length() > 50) {
preview = preview.substring(0, 50) + "...";
if (preview.length() > 100) { // Increased preview length
preview = preview.substring(0, 100) + "...";
}
Conversation conversationToUpdate = database.conversationDao().getConversationById(conversationIdToSave);
if (conversationToUpdate != null) {
@ -182,9 +194,10 @@ public class MainActivity extends AppCompatActivity {
} else {
Log.d(TAG, "No active conversation or no messages to save for convId: " + currentConversationId);
if (currentConversationId != -1 && currentMessageListForSaving.isEmpty()) {
// Optionally delete empty conversation shells if they are created but never used
final long emptyConvId = currentConversationId;
databaseExecutor.execute(() -> {
Log.d(TAG, "Deleting empty conversation shell with ID: " + emptyConvId);
Log.d(TAG, "Attempting to delete empty conversation shell with ID: " + emptyConvId);
database.conversationDao().deleteConversationById(emptyConvId);
});
}
@ -192,25 +205,111 @@ public class MainActivity extends AppCompatActivity {
}
private void openHistoryActivity() {
// Toast.makeText(this, "历史对话功能待实现", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(this, HistoryActivity.class);
startActivity(intent);
Intent intent = new Intent(this, HistoryActivity.class);
startActivity(intent);
}
private List<ChatRequest.Message> buildApiContextMessages(List<ChatMessage> sourceMessages) {
List<ChatRequest.Message> apiMessages = new ArrayList<>();
// Optional: Add a system prompt if desired for your models
// apiMessages.add(new ChatRequest.Message("system", "You are a helpful assistant."));
int startIndex = Math.max(0, sourceMessages.size() - MAX_CONTEXT_MESSAGES);
for (int i = startIndex; i < sourceMessages.size(); i++) {
ChatMessage chatMsg = sourceMessages.get(i);
// Ensure only user and assistant messages are part of the context
if ("user".equals(chatMsg.role) || "assistant".equals(chatMsg.role)) {
apiMessages.add(new ChatRequest.Message(chatMsg.role, chatMsg.content));
}
}
// If after filtering, the context is empty but we have a user message (e.g., first message of conversation)
// This case should be covered if saveMessageAndUpdateUi adds the user message to sourceMessages before this method is called.
if (apiMessages.isEmpty() && !sourceMessages.isEmpty()) {
ChatMessage lastMessageInSource = sourceMessages.get(sourceMessages.size() -1);
if ("user".equals(lastMessageInSource.role)) {
Log.w(TAG, "API context was empty after filtering, adding current user message directly.");
apiMessages.add(new ChatRequest.Message(lastMessageInSource.role, lastMessageInSource.content));
}
}
return apiMessages;
}
private synchronized void incrementActiveApiCallCount() {
activeApiCallCount++;
if (activeApiCallCount == 1) { // Only set loading true when the first call starts
mainThreadHandler.post(() -> setLoadingState(true));
}
Log.d(TAG, "Incremented active API calls: " + activeApiCallCount);
}
private synchronized void decrementActiveApiCallCountAndCheckLoadingState() {
if (activeApiCallCount > 0) {
activeApiCallCount--;
}
Log.d(TAG, "Decremented active API calls: " + activeApiCallCount);
if (activeApiCallCount == 0) {
mainThreadHandler.post(() -> setLoadingState(false));
}
}
private void makeApiCall(ChatRequest request, final String modelIdentifier) {
if (apiService == null) {
handleApiError("API Service not initialized. Please check settings or restart.");
decrementActiveApiCallCountAndCheckLoadingState(); // Ensure counter is decremented
return;
}
String authHeader = "Bearer " + apiKey;
Log.d(TAG, "Making API call to model: " + modelIdentifier + " with request: " + request.toString());
apiService.getChatCompletion(authHeader, request).enqueue(new Callback<ChatResponse>() {
@Override
public void onResponse(@NonNull Call<ChatResponse> call, @NonNull Response<ChatResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().choices != null && !response.body().choices.isEmpty()) {
ChatResponse.Message assistantApiMessage = response.body().choices.get(0).message;
if (assistantApiMessage != null && assistantApiMessage.content != null && assistantApiMessage.role != null) {
String responseContent = "[Model: " + modelIdentifier + "]\n" + assistantApiMessage.content.trim();
ChatMessage assistantMessage = new ChatMessage(
currentConversationId,
assistantApiMessage.role, // Usually "assistant"
responseContent,
System.currentTimeMillis()
);
saveMessageAndUpdateUi(assistantMessage);
} else {
handleApiError("API response format error from " + modelIdentifier + ": Assistant message or role is null.");
}
} else {
String errorBodyString = "Could not read error body.";
if (response.errorBody() != null) {
try {
errorBodyString = response.errorBody().string();
} catch (IOException e) {
Log.e(TAG, "Error reading errorBody from " + modelIdentifier, e);
}
}
handleApiError("API request failed for " + modelIdentifier + ": " + response.code() + " - " + response.message() + "\nDetails: " + errorBodyString);
}
decrementActiveApiCallCountAndCheckLoadingState();
}
@Override
public void onFailure(@NonNull Call<ChatResponse> call, @NonNull Throwable t) {
handleApiError("Network request failed for " + modelIdentifier + ": " + t.getMessage());
Log.e(TAG, "API Call Failed for " + modelIdentifier, t);
decrementActiveApiCallCountAndCheckLoadingState();
}
});
}
// 旧的 generateNewSessionId() 可能不再需要或者其逻辑并入 Conversation 管理
// private String generateNewSessionId() {
// return UUID.randomUUID().toString();
// }
private void sendMessage() {
loadApiSettings(); // 确保API设置是最新的
loadApiSettings(); // Ensure settings are current
if (currentConversationId == -1) {
Toast.makeText(this, "无法发送消息:当前会话无效", Toast.LENGTH_SHORT).show();
Log.e(TAG, "Cannot send message, currentConversationId is -1. Attempting to start a new session.");
// 尝试强制开始一个新会话并提示用户重试
startNewConversationSession();
// UI上给用户一些反馈比如禁用发送按钮直到新会话ID获取成功
Toast.makeText(this, "正在初始化新会话,请稍后重试发送", Toast.LENGTH_LONG).show();
return;
}
@ -222,126 +321,115 @@ public class MainActivity extends AppCompatActivity {
}
String placeholderApiKey = "";
try {placeholderApiKey = getString(R.string.default_api_key_placeholder);} catch (Exception e) {/*already logged*/}
try { placeholderApiKey = getString(R.string.default_api_key_placeholder); } catch (Exception e) { /* already logged */ }
if (apiKey.isEmpty() || apiKey.equals(placeholderApiKey)) {
Toast.makeText(this, getString(R.string.error_api_key_not_set), Toast.LENGTH_LONG).show();
return;
}
// ... 其他 modelName, apiBaseUrl 检查 ...
if (TextUtils.isEmpty(apiBaseUrl)) {
Toast.makeText(this, getString(R.string.error_base_url_not_set), Toast.LENGTH_LONG).show();
return;
}
// Model 1 is mandatory (or should have a default from settings)
if (TextUtils.isEmpty(modelName1)) {
Toast.makeText(this, "主模型 (模型1) 未配置,请检查设置。", Toast.LENGTH_LONG).show();
return;
}
// 假设 ChatMessage 构造函数已更新为 (long conversationId, String role, String content, long timestamp)
ChatMessage userMessage = new ChatMessage(currentConversationId, "user", messageText, System.currentTimeMillis());
// Save user message and update UI immediately.
// The saveMessageAndUpdateUi method adds to currentMessageListForSaving.
saveMessageAndUpdateUi(userMessage);
editTextMessage.setText("");
setLoadingState(true);
editTextMessage.setText(""); // Clear input field
List<ChatRequest.Message> apiContextMessages = new ArrayList<>();
// currentMessageListForSaving 构建上下文确保包含最新的用户消息
List<ChatMessage> contextSource = new ArrayList<>(currentMessageListForSaving); // currentMessageListForSaving 应已包含刚发送的userMessage
// Build context messages *after* user message is added to currentMessageListForSaving
List<ChatRequest.Message> apiContextMessages = buildApiContextMessages(new ArrayList<>(currentMessageListForSaving));
int startIndex = Math.max(0, contextSource.size() - MAX_CONTEXT_MESSAGES);
for (int i = startIndex; i < contextSource.size(); i++) {
ChatMessage chatMsg = contextSource.get(i);
apiContextMessages.add(new ChatRequest.Message(chatMsg.role, chatMsg.content));
}
// 如果 saveMessageAndUpdateUi 是完全异步的并且 currentMessageListForSaving 可能还没更新
// 则需要确保 userMessage 被显式添加到 apiContextMessages但你的 saveMessageAndUpdateUi 看起来会同步更新 currentMessageListForSaving 的引用
// 为保险起见可以检查最后一条是否是当前用户消息如果不是则添加
// 但更稳妥的方式是 saveMessageAndUpdateUi完成后再构建apiContextMessages或者传递userMessage
// 这里我们遵循你提供的逻辑它在循环后又添加了一次
// 但你的示例代码中循环后添加userMessage.content 的部分被注释掉了我将它恢复并确保它使用的是最新的列表
// 实际上如果 saveMessageAndUpdateUi 立即将 userMessage 添加到 currentMessageListForSaving
// 那么下面的循环就应该能取到它为清晰起见我们直接从 currentMessageListForSaving 构建
// Reset active call count before making new calls
// This should be handled carefully if sendMessage can be called rapidly.
// For simplicity, assuming one "send operation" at a time.
// The increment/decrement logic handles the loading state.
if (apiContextMessages.isEmpty() && userMessage != null) { // 如果列表为空但我们确实有用户消息
Log.w(TAG, "API context was empty, adding current user message directly.");
apiContextMessages.add(new ChatRequest.Message(userMessage.role, userMessage.content));
boolean callMade = false;
// Send to Model 1
if (!TextUtils.isEmpty(modelName1)) {
Log.d(TAG, "Preparing to send message to Model 1: " + modelName1);
ChatRequest request1 = new ChatRequest(modelName1, apiContextMessages);
incrementActiveApiCallCount(); // Increment before making the call
makeApiCall(request1, modelName1);
callMade = true;
}
// Send to Model 2 if configured
if (!TextUtils.isEmpty(modelName2)) {
Log.d(TAG, "Preparing to send message to Model 2: " + modelName2);
// It's crucial that apiContextMessages is the same for both calls if comparing responses
ChatRequest request2 = new ChatRequest(modelName2, apiContextMessages);
incrementActiveApiCallCount(); // Increment before making the call
makeApiCall(request2, modelName2);
callMade = true;
}
ChatRequest request = new ChatRequest(modelName, apiContextMessages);
String authHeader = "Bearer " + apiKey;
// apiService = ApiClient.getClient(this).create(DeepSeekApiService.class); // 确保apiService已初始化
apiService.getChatCompletion(authHeader, request).enqueue(new Callback<ChatResponse>() {
@Override
public void onResponse(@NonNull Call<ChatResponse> call, @NonNull Response<ChatResponse> response) {
mainThreadHandler.post(() -> setLoadingState(false));
if (response.isSuccessful() && response.body() != null && response.body().choices != null && !response.body().choices.isEmpty()) {
ChatResponse.Message assistantApiMessage = response.body().choices.get(0).message;
if (assistantApiMessage != null && assistantApiMessage.content != null && assistantApiMessage.role != null) {
ChatMessage assistantMessage = new ChatMessage(
currentConversationId, // 使用当前会话ID
assistantApiMessage.role,
assistantApiMessage.content.trim(),
System.currentTimeMillis()
);
saveMessageAndUpdateUi(assistantMessage);
} else {
handleApiError("API 响应格式错误: 助手消息或角色为空");
}
} else {
String errorBodyString = "无法读取错误详情";
if (response.errorBody() != null) {
try {
errorBodyString = response.errorBody().string();
} catch (IOException e) {
Log.e(TAG, "读取 errorBody 失败", e);
}
}
handleApiError("API 请求失败: " + response.code() + " - " + response.message() + "\n详情: " + errorBodyString);
}
}
@Override
public void onFailure(@NonNull Call<ChatResponse> call, @NonNull Throwable t) {
mainThreadHandler.post(() -> setLoadingState(false));
handleApiError("网络请求失败: " + t.getMessage());
Log.e(TAG, "API Call Failed", t);
}
});
if (!callMade) {
// This case should ideally not be reached if modelName1 is always set.
handleApiError("没有配置任何模型,请在设置中选择模型。");
// No need to call setLoadingState(false) here, as it wasn't set to true
}
}
private void saveMessageAndUpdateUi(ChatMessage message) {
// 确保消息关联到当前会话
if (message.conversationId == -1 && currentConversationId != -1) {
message.conversationId = currentConversationId;
} else if (message.conversationId == -1 && currentConversationId == -1) {
Log.e(TAG, "Message cannot be saved. No active conversation session (convId: -1). Attempting to start new session.");
Toast.makeText(this, "错误:无法保存消息,会话无效。正在尝试创建新会话...", Toast.LENGTH_LONG).show();
startNewConversationSession(); // 尝试启动然后让用户重试发送
return; // 阻止当前消息被处理因为会话ID无效
Log.e(TAG, "Message cannot be saved. No active conversation session (convId: -1).");
Toast.makeText(this, "错误:无法保存消息,会话无效。", Toast.LENGTH_LONG).show();
// Optionally, try to start a new session, but this might lead to lost messages if not handled carefully
// startNewConversationSession();
return;
}
final ChatMessage messageToSave = message;
databaseExecutor.execute(() -> {
long insertedMessageId = database.chatMessageDao().insert(messageToSave);
messageToSave.id = (int) insertedMessageId;
messageToSave.id = (int) insertedMessageId; // Update the object with the DB-generated ID
mainThreadHandler.post(() -> {
if (chatAdapter != null) {
chatAdapter.addMessage(messageToSave);
}
currentMessageListForSaving.add(messageToSave); // 添加到当前会话的内存列表
// Add to in-memory list only if it's not already there (e.g. if called for user message then assistant)
// A better check might be needed if messages could be added out of order or duplicated.
// For now, assuming it's added sequentially.
if (!currentMessageListForSaving.contains(messageToSave)) { // Basic check to avoid duplicates
currentMessageListForSaving.add(messageToSave);
}
scrollToBottom(true);
Log.d(TAG, "Message saved to DB and UI. ConvID: " + messageToSave.conversationId + ", MsgID: " + messageToSave.id + ". Adapter count: " + (chatAdapter != null ? chatAdapter.getItemCount() : "null"));
Log.d(TAG, "Message saved to DB and UI. ConvID: " + messageToSave.conversationId +
", MsgID: " + messageToSave.id +
", Role: " + messageToSave.role +
". Adapter count: " + (chatAdapter != null ? chatAdapter.getItemCount() : "null"));
});
});
}
private void loadChatHistory() {
Log.d(TAG, "loadChatHistory called - starting new conversation session for MainActivity.");
startNewConversationSession(); // MainActivity 总是开始一个新对话
setLoadingState(false);
startNewConversationSession(); // MainActivity always starts a new conversation on create
setLoadingState(false); // Ensure loading is off initially
}
private void handleApiError(String errorMessage) {
mainThreadHandler.post(() -> {
Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
Log.e(TAG, errorMessage);
setLoadingState(false); // 确保在API错误时也解除加载状态
Log.e(TAG, "API_ERROR: " + errorMessage);
// Decrement counter is handled by makeApiCall's onFailure,
// but if error happens before makeApiCall, ensure loading state is reset.
// For errors like "No model configured", loading state might not have been set.
// if (activeApiCallCount == 0) setLoadingState(false);
});
}
@ -357,6 +445,7 @@ public class MainActivity extends AppCompatActivity {
}
private void setLoadingState(boolean isLoading) {
Log.d(TAG, "Setting loading state to: " + isLoading);
if (isLoading) {
progressBar.setVisibility(View.VISIBLE);
buttonSend.setEnabled(false);
@ -372,21 +461,7 @@ public class MainActivity extends AppCompatActivity {
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_main, menu);
// 根据新的逻辑刷新按钮可以调用 refreshConversation()
MenuItem refreshItem = menu.findItem(R.id.action_refresh_conversation); // 假设你的menu.xml中有这个ID
if (refreshItem != null) {
refreshItem.setOnMenuItemClickListener(item -> {
refreshConversation();
return true;
});
}
MenuItem historyItem = menu.findItem(R.id.action_history); // 假设你的menu.xml中有这个ID
if (historyItem != null) {
historyItem.setOnMenuItemClickListener(item -> {
openHistoryActivity();
return true;
});
}
// Removed direct listeners as they are handled in onOptionsItemSelected
return true;
}
@ -397,19 +472,13 @@ public class MainActivity extends AppCompatActivity {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
} else if (itemId == R.id.action_refresh_conversation) {
refreshConversation();
return true;
} else if (itemId == R.id.action_history) {
openHistoryActivity();
return true;
}
// 对于 action_refresh_conversation action_history
// 如果你希望它们保留在溢出菜单中并通过 onOptionsItemSelected 处理
// 而不是像上面 onCreateOptionsMenu 中那样直接设置监听器
// 那么这里的逻辑可以保留或调整
// 为清晰起见如果已在 onCreateOptionsMenu 中处理这里可以移除
// else if (itemId == R.id.action_refresh_conversation) {
// refreshConversation();
// return true;
// } else if (itemId == R.id.action_history) {
// openHistoryActivity();
// return true;
// }
return super.onOptionsItemSelected(item);
}
@ -423,7 +492,7 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy called. Saving current conversation if needed before shutdown.");
saveCurrentConversationIfNeeded();
saveCurrentConversationIfNeeded(); // Ensure last save attempt
if (databaseExecutor != null && !databaseExecutor.isShutdown()) {
databaseExecutor.shutdown();
}

View File

@ -6,8 +6,9 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.view.MenuItem;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView; // Import AutoCompleteTextView
import android.widget.Button;
import android.widget.Spinner;
// import android.widget.Spinner; // Spinner is no longer used
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
@ -24,16 +25,32 @@ public class SettingsActivity extends AppCompatActivity {
public static final String SHARED_PREFS_NAME = "AppSettings";
public static final String KEY_API_KEY = "apiKey";
public static final String KEY_API_BASE_URL = "apiBaseUrl";
public static final String KEY_SELECTED_MODEL = "selectedModel";
// public static final String KEY_SELECTED_MODEL = "selectedModel"; // Old key, no longer primary
public static final String DEFAULT_API_BASE_URL = "https://api.deepseek.com/";
public static final String DEFAULT_MODEL = "deepseek-chat";
public static final List<String> AVAILABLE_MODELS = Arrays.asList("deepseek-chat", "deepseek-coder", "deepseek-reasoner", "gpt-3.5-turbo", "gpt-4o-mini");
// New SharedPreferences Keys for two models
public static final String KEY_SELECTED_MODEL_1 = "selectedModel1";
public static final String KEY_SELECTED_MODEL_2 = "selectedModel2";
public static final String DEFAULT_API_BASE_URL = "https://api.deepseek.com/"; // Or your preferred default
// Default models
public static final String DEFAULT_MODEL_1 = "gpt-4o-mini"; // Example default for model 1
public static final String DEFAULT_MODEL_2 = ""; // Default for model 2 (empty means not used)
// Available models list (can be shared by both AutoCompleteTextViews)
public static final List<String> AVAILABLE_MODELS = Arrays.asList(
"deepseek-chat", "deepseek-reasoner", // DeepSeek models
"gpt-4o-mini", "gpt-3.5-turbo", // OpenAI models
"gemini-1.5-flash-latest", "gemini-1.5-pro-latest", // Google Gemini models
"claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307" // Anthropic Claude models
// Add other models as needed
);
private TextInputEditText editTextApiKey;
private TextInputEditText editTextApiBaseUrl;
private Spinner spinnerModel;
// private Spinner spinnerModel; // Spinner is no longer used
private AutoCompleteTextView autoCompleteTextViewModel1; // New
private AutoCompleteTextView autoCompleteTextViewModel2; // New
private Button buttonSaveSettings;
private SharedPreferences sharedPreferences;
@ -41,104 +58,114 @@ public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings); // Make sure this layout does not try to use a Toolbar that will conflict
setContentView(R.layout.activity_settings);
// --- MODIFICATION START: Comment out custom Toolbar setup ---
/*
Toolbar toolbar = findViewById(R.id.toolbar_settings); // Assuming R.id.toolbar_settings is in activity_settings.xml
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // For the back button
getSupportActionBar().setDisplayShowHomeEnabled(true); // For the back button
// getSupportActionBar().setTitle(getString(R.string.title_activity_settings)); // Optional: Set title if needed
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
*/
// --- MODIFICATION END ---
// If using system ActionBar, and you want a back arrow,
// ensure parentActivityName is set in AndroidManifest.xml for this Activity,
// or rely on onOptionsItemSelected for android.R.id.home.
// The title will be set by the system based on the activity's label in the manifest,
// or you can try to set it on the system ActionBar if needed, after super.onCreate() and setContentView().
// For example, if you still want to try setting title on system action bar:
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Request back arrow on system ActionBar
// getSupportActionBar().setTitle(getString(R.string.your_settings_title_resource)); // Set title if needed
}
sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
editTextApiKey = findViewById(R.id.editTextApiKey);
editTextApiBaseUrl = findViewById(R.id.editTextApiBaseUrl);
spinnerModel = findViewById(R.id.spinnerModel);
// spinnerModel = findViewById(R.id.spinnerModel); // Spinner is no longer used
autoCompleteTextViewModel1 = findViewById(R.id.autoCompleteTextViewModel1); // Initialize new view
autoCompleteTextViewModel2 = findViewById(R.id.autoCompleteTextViewModel2); // Initialize new view
buttonSaveSettings = findViewById(R.id.buttonSaveSettings);
setupSpinner();
// setupSpinner(); // Old spinner setup is no longer needed
setupAutoCompleteModelViews(); // New setup for AutoCompleteTextViews
loadSettings();
buttonSaveSettings.setOnClickListener(v -> saveSettings());
}
private void setupSpinner() {
// New method to set up both AutoCompleteTextViews
private void setupAutoCompleteModelViews() {
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, AVAILABLE_MODELS);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerModel.setAdapter(adapter);
android.R.layout.simple_dropdown_item_1line, AVAILABLE_MODELS);
autoCompleteTextViewModel1.setAdapter(adapter);
autoCompleteTextViewModel2.setAdapter(adapter); // Both can share the same adapter and list
}
private void loadSettings() {
String apiKey = sharedPreferences.getString(KEY_API_KEY, "");
String apiBaseUrl = sharedPreferences.getString(KEY_API_BASE_URL, DEFAULT_API_BASE_URL);
String selectedModel = sharedPreferences.getString(KEY_SELECTED_MODEL, DEFAULT_MODEL);
// Load settings for the two models
String selectedModel1 = sharedPreferences.getString(KEY_SELECTED_MODEL_1, DEFAULT_MODEL_1);
String selectedModel2 = sharedPreferences.getString(KEY_SELECTED_MODEL_2, DEFAULT_MODEL_2);
editTextApiKey.setText(apiKey);
editTextApiBaseUrl.setText(apiBaseUrl);
// Set text for AutoCompleteTextViews. The 'false' argument prevents filtering.
autoCompleteTextViewModel1.setText(selectedModel1, false);
autoCompleteTextViewModel2.setText(selectedModel2, false);
// Old spinner loading logic is removed
/*
int modelPosition = AVAILABLE_MODELS.indexOf(selectedModel);
if (modelPosition >= 0) {
spinnerModel.setSelection(modelPosition);
} else if (!AVAILABLE_MODELS.isEmpty()) {
spinnerModel.setSelection(0);
}
*/
}
private void saveSettings() {
String apiKey = editTextApiKey.getText() != null ? editTextApiKey.getText().toString().trim() : "";
String apiBaseUrl = editTextApiBaseUrl.getText() != null ? editTextApiBaseUrl.getText().toString().trim() : DEFAULT_API_BASE_URL;
String selectedModel = spinnerModel.getSelectedItem() != null ? spinnerModel.getSelectedItem().toString() : DEFAULT_MODEL;
// Get values from AutoCompleteTextViews
String model1 = autoCompleteTextViewModel1.getText().toString().trim();
String model2 = autoCompleteTextViewModel2.getText().toString().trim();
if (TextUtils.isEmpty(apiKey)) {
Toast.makeText(this, getString(R.string.toast_api_key_required), Toast.LENGTH_SHORT).show();
// Consider using getString(R.string.toast_api_key_required) if you have it
Toast.makeText(this, "API Key cannot be empty", Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.isEmpty(apiBaseUrl)) {
apiBaseUrl = DEFAULT_API_BASE_URL;
apiBaseUrl = DEFAULT_API_BASE_URL; // Fallback to default if empty
editTextApiBaseUrl.setText(apiBaseUrl);
}
// Ensure base URL ends with a slash
if (!apiBaseUrl.endsWith("/")) {
apiBaseUrl += "/";
editTextApiBaseUrl.setText(apiBaseUrl);
editTextApiBaseUrl.setText(apiBaseUrl); // Update UI
}
// Handle model1: if empty, use default.
if (TextUtils.isEmpty(model1)) {
model1 = DEFAULT_MODEL_1;
autoCompleteTextViewModel1.setText(model1, false); // Update UI
}
// model2 can be empty, indicating it's not used.
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(KEY_API_KEY, apiKey);
editor.putString(KEY_API_BASE_URL, apiBaseUrl);
editor.putString(KEY_SELECTED_MODEL, selectedModel);
// Save the two models
editor.putString(KEY_SELECTED_MODEL_1, model1);
editor.putString(KEY_SELECTED_MODEL_2, model2);
// editor.putString(KEY_SELECTED_MODEL, selectedModel); // Old key no longer primary
editor.apply();
ApiClient.resetClient();
ApiClient.resetClient(); // Reset if your ApiClient caches settings
Toast.makeText(this, getString(R.string.toast_settings_saved), Toast.LENGTH_SHORT).show();
// Consider using getString(R.string.toast_settings_saved)
Toast.makeText(this, "Settings saved", Toast.LENGTH_SHORT).show();
finish();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// This handles the back arrow click if it's an action item (like from a system ActionBar)
if (item.getItemId() == android.R.id.home) {
finish(); // Go back to the previous activity
finish();
return true;
}
return super.onOptionsItemSelected(item);

View File

@ -1,6 +1,6 @@
package com.lotus_lab.jetchat.ui;
import android.util.Log; // Import Log
import android.util.Log; // 导入 Log
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -12,17 +12,18 @@ import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.lotus_lab.jetchat.R;
import com.lotus_lab.jetchat.data.ChatMessage; // 确保 ChatMessage role 属性
import com.lotus_lab.jetchat.data.ChatMessage; // 确保 ChatMessage role 属性
import java.util.ArrayList;
import java.util.List;
public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.MessageViewHolder> {
private static final String ADAPTER_TAG = "ChatAdapter"; // Tag for logging
private static final String ADAPTER_TAG = "ChatAdapter"; // 用于日志记录的标签
private final List<ChatMessage> messages;
// ChatMessage 类中定义这些常量或者在这里定义以避免硬编码字符串 "assistant" "user"
// 可以 ChatMessage 类中定义这些常量或者在这里定义以避免硬编码字符串 "assistant" "user"
// public static final String ROLE_USER = "user";
// public static final String ROLE_ASSISTANT = "assistant";
@ -34,38 +35,38 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.MessageViewHol
int oldSize = messages.size();
this.messages.clear();
notifyItemRangeRemoved(0, oldSize);
Log.d(ADAPTER_TAG, "Messages cleared. Old size: " + oldSize);
Log.d(ADAPTER_TAG, "消息已清空。旧数量: " + oldSize);
}
public List<ChatMessage> getMessages() {
return new ArrayList<>(this.messages); // Return a copy
return new ArrayList<>(this.messages); // 返回列表的副本
}
public void setMessages(List<ChatMessage> newMessages) {
this.messages.clear();
if (newMessages != null) { // Add null check for safety
if (newMessages != null) { // 为安全起见添加 null 检查
this.messages.addAll(newMessages);
}
notifyDataSetChanged(); // Consider using DiffUtil for better performance in production
Log.d(ADAPTER_TAG, "Messages set. New count: " + this.messages.size());
notifyDataSetChanged(); // 在生产环境中考虑使用 DiffUtil 以获得更好的性能
Log.d(ADAPTER_TAG, "消息已设置。新数量: " + this.messages.size());
}
public void addMessage(ChatMessage message) {
if (message == null) {
Log.w(ADAPTER_TAG, "Attempted to add a null message.");
Log.w(ADAPTER_TAG, "尝试添加一个 null 消息。");
return;
}
this.messages.add(message);
notifyItemInserted(messages.size() - 1);
Log.d(ADAPTER_TAG, "Message added. Role: " + (message.role != null ? message.role : "null") +
", Content: '" + (message.content != null ? message.content : "null") +
"'. New count: " + messages.size());
Log.d(ADAPTER_TAG, "消息已添加。角色: " + (message.role != null ? message.role : "null") +
", 内容: '" + (message.content != null ? message.content.substring(0, Math.min(message.content.length(), 50))+"..." : "null") + // 截断过长的内容日志
"'. 新数量: " + messages.size());
}
@NonNull
@Override
public MessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Log.d(ADAPTER_TAG, "onCreateViewHolder called for viewType: " + viewType);
Log.d(ADAPTER_TAG, "onCreateViewHolder 调用,视图类型: " + viewType);
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_message, parent, false);
return new MessageViewHolder(view);
@ -76,73 +77,74 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.MessageViewHol
ChatMessage message = messages.get(position);
Log.i(ADAPTER_TAG, "-----------------------------------------------------");
Log.i(ADAPTER_TAG, "onBindViewHolder - Position: " + position);
Log.i(ADAPTER_TAG, "onBindViewHolder - 位置: " + position);
if (message == null) {
Log.e(ADAPTER_TAG, "Message object is NULL at position " + position + ". Hiding item.");
holder.itemView.setVisibility(View.GONE); // Hide the item if message is null
Log.e(ADAPTER_TAG, "位置 " + position + " 的 Message 对象为 NULL。正在隐藏项目。");
holder.itemView.setVisibility(View.GONE); // 如果消息为 null则隐藏该项目
return;
}
// Log the raw role and content from the ChatMessage object
Log.d(ADAPTER_TAG, "Raw Message Role: '" + message.role + "'");
Log.d(ADAPTER_TAG, "Raw Message Content: '" + message.content + "'");
// 记录 ChatMessage 对象的原始角色和内容
Log.d(ADAPTER_TAG, "原始消息角色: '" + message.role + "'");
Log.d(ADAPTER_TAG, "原始消息内容: '" + message.content + "'");
if (message.role == null || message.content == null) {
holder.itemView.setVisibility(View.GONE);
Log.w(ADAPTER_TAG, "Message role or content is null at position " + position + ". Role: '" + message.role + "', Content: '" + message.content + "'. Hiding item.");
Log.w(ADAPTER_TAG, "位置 " + position + " 的消息角色或内容为 null。角色: '" + message.role + "', 内容: '" + message.content + "'. 正在隐藏项目。");
return;
}
// Ensure item is visible if data is valid
// 如果数据有效确保项目可见
holder.itemView.setVisibility(View.VISIBLE);
// Optional: Reset item background if you were setting alternating colors for the whole item in debug
// 可选如果在调试中为整个项目设置了交替颜色则重置项目背景
// holder.itemView.setBackground(null);
// Crucial check: compare role with "assistant" and "user"
// Make sure these strings EXACTLY match what's in your ChatMessage.role
// 关键检查将角色与 "assistant" "user" 进行比较
// 确保这些字符串与 ChatMessage.role 中的内容完全匹配
boolean isAiMessage = "assistant".equals(message.role);
// boolean isUserMessage = "user".equals(message.role); // You can also explicitly check for user
// boolean isUserMessage = "user".equals(message.role); // 也可以显式检查用户消息
Log.d(ADAPTER_TAG, "Processed Role: '" + message.role + "', isAiMessage = " + isAiMessage);
Log.d(ADAPTER_TAG, "处理后的角色: '" + message.role + "', 是否为AI消息 = " + isAiMessage);
// 直接设置文本内容它已经包含了模型标识例如 "[模型: xxx]\n实际内容..."
holder.tvMessage.setText(message.content);
holder.tvMessage.setVisibility(View.VISIBLE); // Ensure TextView is visible
holder.tvMessage.setVisibility(View.VISIBLE); // 确保 TextView 可见
// Reset text color and background to defaults from XML or a base style,
// in case previous debug styles are cached by RecyclerView's ViewHolder.
holder.tvMessage.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.black)); // Or your R.color.your_default_text_color
// holder.tvMessage.setBackground(null); // Let specific backgrounds below take precedence
// 将文本颜色和背景重置为 XML 或基本样式中的默认值
// 以防 RecyclerView ViewHolder 缓存了之前的调试样式
// holder.tvMessage.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.black)); // 或者你的 R.color.your_default_text_color
// holder.tvMessage.setBackground(null); // 让下面的特定背景优先
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) holder.tvMessage.getLayoutParams();
if (isAiMessage) {
Log.d(ADAPTER_TAG, "Applying AI message style for: '" + message.content.substring(0, Math.min(message.content.length(), 20)) + "...'");
Log.d(ADAPTER_TAG, "正在为AI消息应用样式: '" + message.content.substring(0, Math.min(message.content.length(), 20)) + "...'");
holder.ivAiAvatar.setVisibility(View.VISIBLE);
holder.ivUserAvatar.setVisibility(View.GONE);
params.horizontalBias = 0.0f; // AI 消息靠左
holder.tvMessage.setBackground(
ContextCompat.getDrawable(holder.itemView.getContext(), R.drawable.bg_reply_message_bubble)
);
// Example: Different text color for AI messages if needed
// 示例如果需要可以为 AI 消息设置不同的文本颜色
// holder.tvMessage.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.ai_text_color));
} else { // Assuming any non-AI message is a user message for now
// If you have other roles like "system", you'll need more specific checks.
Log.d(ADAPTER_TAG, "Applying User message style for: '" + message.content.substring(0, Math.min(message.content.length(), 20)) + "...'");
} else { // 目前假设任何非 AI 消息都是用户消息
// 如果有其他角色 "system"则需要更具体的检查
Log.d(ADAPTER_TAG, "正在为用户消息应用样式: '" + message.content.substring(0, Math.min(message.content.length(), 20)) + "...'");
holder.ivAiAvatar.setVisibility(View.GONE);
holder.ivUserAvatar.setVisibility(View.GONE); // As per your item_message.xml, user avatar is GONE by default
holder.ivUserAvatar.setVisibility(View.GONE); // 根据你的 item_message.xml用户头像默认为 GONE
params.horizontalBias = 1.0f; // 用户消息靠右
holder.tvMessage.setBackground(
ContextCompat.getDrawable(holder.itemView.getContext(), R.drawable.bg_user_message_bubble)
);
// Example: Different text color for user messages if needed
// 示例如果需要可以为用户消息设置不同的文本颜色
// holder.tvMessage.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.user_text_color));
}
holder.tvMessage.setLayoutParams(params);
Log.i(ADAPTER_TAG, "Finished applying style for position: " + position);
Log.i(ADAPTER_TAG, "已完成为位置 " + position + " 应用样式");
Log.i(ADAPTER_TAG, "-----------------------------------------------------");
}
@ -159,12 +161,13 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.MessageViewHol
MessageViewHolder(@NonNull View itemView) {
super(itemView);
ivAiAvatar = itemView.findViewById(R.id.iv_ai_avatar);
ivUserAvatar = itemView.findViewById(R.id.iv_user_avatar);
ivUserAvatar = itemView.findViewById(R.id.iv_user_avatar); // 虽然用户头像默认GONE但ID还是需要的
tvMessage = itemView.findViewById(R.id.tv_message);
if (ivAiAvatar == null) Log.e(ADAPTER_TAG, "MessageViewHolder: ivAiAvatar is null!");
if (ivUserAvatar == null) Log.e(ADAPTER_TAG, "MessageViewHolder: ivUserAvatar is null!");
if (tvMessage == null) Log.e(ADAPTER_TAG, "MessageViewHolder: tvMessage is null!");
if (ivAiAvatar == null) Log.e(ADAPTER_TAG, "MessageViewHolder: ivAiAvatar 为 null!");
// ivUserAvatar 在你的布局中可能确实不存在或ID不同如果它总是GONE可以不查找或处理null
// if (ivUserAvatar == null) Log.e(ADAPTER_TAG, "MessageViewHolder: ivUserAvatar 为 null!");
if (tvMessage == null) Log.e(ADAPTER_TAG, "MessageViewHolder: tvMessage 为 null!");
}
}
}

View File

@ -59,7 +59,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/toolbar" app:layout_constraintBottom_toTopOf="@+id/inputLayout"
android:indeterminateTint="@color/purple_500"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintBottom_toTopOf="@+id/inputLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible"/>

View File

@ -54,19 +54,51 @@
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 模型 1 选择 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_select_model"
android:text="模型 1 (主要)"
android:textSize="16sp"
android:layout_marginBottom="8dp"/>
<Spinner
android:id="@+id/spinnerModel"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutModel1"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="选择或输入模型 1">
<AutoCompleteTextView
android:id="@+id/autoCompleteTextViewModel1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<!-- 模型 2 选择 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="模型 2 (可选, 用于对比)"
android:textSize="16sp"
android:layout_marginBottom="8dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutModel2"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:minHeight="48dp"/>
android:hint="选择或输入模型 2 (留空则只用模型1)">
<AutoCompleteTextView
android:id="@+id/autoCompleteTextViewModel2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/buttonSaveSettings"

View File

@ -38,6 +38,7 @@
android:maxWidth="280dp"
android:padding="8dp"
android:textColor="@android:color/black"
android:textIsSelectable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/iv_user_avatar"
app:layout_constraintHorizontal_bias="0.0"

View File

@ -7,12 +7,13 @@
<string name="settings_title">设置</string>
<string name="hint_api_key">API 密钥</string>
<string name="hint_api_base_url">API 基础 URL (例如 https://api.example.com/)</string>
<string name="label_select_model">选择模型</string>
<string name="label_select_model">选择模型</string> <!-- 这个可能不再直接使用,但保留 -->
<string name="button_save_settings">保存设置</string>
<string name="toast_settings_saved">设置已保存</string>
<string name="toast_api_key_required">API 密钥不能为空</string>
<string name="menu_settings">设置</string>
<string name="default_api_key_placeholder">sk-YOUR_DEEPSEEK_API_KEY</string> <string name="error_api_key_not_set">请在设置中配置API密钥</string>
<string name="default_api_key_placeholder">sk-YOUR_DEEPSEEK_API_KEY</string>
<string name="error_api_key_not_set">请在设置中配置API密钥</string>
<string name="error_model_not_set">请在设置中选择一个模型</string>
<string name="error_base_url_not_set">请在设置中配置API基础URL</string>
@ -24,5 +25,13 @@
<string name="menu_refresh_conversation">刷新对话</string>
<string name="menu_history">对话历史</string>
<string name="hint_custom_model_name">自定义模型名称 (如果为空则使用下方选择)</string> <!-- 这个可能不再直接使用,但保留 -->
<string name="label_or_select_preset_model">从预设中选择:</string> <!-- 这个可能不再直接使用,但保留 -->
<!-- 为新的双模型选择添加的字符串 -->
<string name="label_model_1_primary">模型 1 (主要)</string>
<string name="hint_select_or_input_model_1">选择或输入模型 1</string>
<string name="label_model_2_secondary">模型 2 (可选, 用于对比)</string>
<string name="hint_select_or_input_model_2">选择或输入模型 2 (留空则只用模型1)</string>
</resources>