v1.2增加了双对话功能
This commit is contained in:
parent
8940aba889
commit
addbabeb8a
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"/>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user