From addbabeb8ab0d24064baea12b8ced716ee711a65 Mon Sep 17 00:00:00 2001 From: lotus Date: Wed, 7 May 2025 19:39:10 +0800 Subject: [PATCH] =?UTF-8?q?v1.2=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=8F=8C?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/lotus_lab/jetchat/MainActivity.java | 369 +++++++++++------- .../lotus_lab/jetchat/SettingsActivity.java | 115 +++--- .../com/lotus_lab/jetchat/ui/ChatAdapter.java | 91 ++--- src/main/res/layout/activity_main.xml | 4 +- src/main/res/layout/activity_settings.xml | 40 +- src/main/res/layout/item_message.xml | 1 + src/main/res/values/strings.xml | 13 +- 7 files changed, 388 insertions(+), 245 deletions(-) diff --git a/src/main/java/com/lotus_lab/jetchat/MainActivity.java b/src/main/java/com/lotus_lab/jetchat/MainActivity.java index 6b21182..91ac588 100644 --- a/src/main/java/com/lotus_lab/jetchat/MainActivity.java +++ b/src/main/java/com/lotus_lab/jetchat/MainActivity.java @@ -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 currentMessageListForSaving = new ArrayList<>(); // 存储当前对话的消息 + private long currentConversationId = -1; + private List 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 messagesToSave = new ArrayList<>(currentMessageListForSaving); + final List 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 buildApiContextMessages(List sourceMessages) { + List 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() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response 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 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 apiContextMessages = new ArrayList<>(); - // 从 currentMessageListForSaving 构建上下文,确保包含最新的用户消息 - List contextSource = new ArrayList<>(currentMessageListForSaving); // currentMessageListForSaving 应已包含刚发送的userMessage + // Build context messages *after* user message is added to currentMessageListForSaving + List 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() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response 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 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(); } diff --git a/src/main/java/com/lotus_lab/jetchat/SettingsActivity.java b/src/main/java/com/lotus_lab/jetchat/SettingsActivity.java index d65de59..6404b8f 100644 --- a/src/main/java/com/lotus_lab/jetchat/SettingsActivity.java +++ b/src/main/java/com/lotus_lab/jetchat/SettingsActivity.java @@ -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 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 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 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); diff --git a/src/main/java/com/lotus_lab/jetchat/ui/ChatAdapter.java b/src/main/java/com/lotus_lab/jetchat/ui/ChatAdapter.java index dd7f1c8..713b6e2 100644 --- a/src/main/java/com/lotus_lab/jetchat/ui/ChatAdapter.java +++ b/src/main/java/com/lotus_lab/jetchat/ui/ChatAdapter.java @@ -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 { - private static final String ADAPTER_TAG = "ChatAdapter"; // Tag for logging + private static final String ADAPTER_TAG = "ChatAdapter"; // 用于日志记录的标签 + private final List 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 getMessages() { - return new ArrayList<>(this.messages); // Return a copy + return new ArrayList<>(this.messages); // 返回列表的副本 } public void setMessages(List 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 diff --git a/src/main/res/layout/activity_settings.xml b/src/main/res/layout/activity_settings.xml index 5133d33..014802e 100644 --- a/src/main/res/layout/activity_settings.xml +++ b/src/main/res/layout/activity_settings.xml @@ -54,19 +54,51 @@ android:inputType="textUri" /> + - + + + + + + + + + android:hint="选择或输入模型 2 (留空则只用模型1)"> + + +