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.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils; // Import TextUtils
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
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.AppDatabase;
|
||||||
import com.lotus_lab.jetchat.data.ChatMessage;
|
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.ApiClient;
|
||||||
import com.lotus_lab.jetchat.network.ChatRequest;
|
import com.lotus_lab.jetchat.network.ChatRequest;
|
||||||
import com.lotus_lab.jetchat.network.ChatResponse;
|
import com.lotus_lab.jetchat.network.ChatResponse;
|
||||||
@ -33,7 +34,7 @@ import com.lotus_lab.jetchat.ui.ChatAdapter;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ import retrofit2.Response;
|
|||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private static final String TAG = "MainActivity";
|
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 RecyclerView recyclerViewChat;
|
||||||
private EditText editTextMessage;
|
private EditText editTextMessage;
|
||||||
@ -57,16 +58,17 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private ExecutorService databaseExecutor;
|
private ExecutorService databaseExecutor;
|
||||||
private Handler mainThreadHandler;
|
private Handler mainThreadHandler;
|
||||||
|
|
||||||
// 用于会话管理
|
private long currentConversationId = -1;
|
||||||
private long currentConversationId = -1; // -1 表示新对话或未初始化的对话
|
private List<ChatMessage> currentMessageListForSaving = new ArrayList<>();
|
||||||
private List<ChatMessage> currentMessageListForSaving = new ArrayList<>(); // 存储当前对话的消息
|
|
||||||
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private String apiKey;
|
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 apiBaseUrl;
|
||||||
// private String currentSessionId = null; // 旧的 sessionId 逻辑,将被 currentConversationId 替代大部分功能
|
|
||||||
|
|
||||||
|
private int activeApiCallCount = 0; // Counter for active API calls
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@ -74,7 +76,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
sharedPreferences = getSharedPreferences(SettingsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE);
|
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());
|
mainThreadHandler = new Handler(Looper.getMainLooper());
|
||||||
recyclerViewChat = findViewById(R.id.recyclerViewChat);
|
recyclerViewChat = findViewById(R.id.recyclerViewChat);
|
||||||
@ -82,18 +84,14 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
buttonSend = findViewById(R.id.buttonSend);
|
buttonSend = findViewById(R.id.buttonSend);
|
||||||
progressBar = findViewById(R.id.progressBar);
|
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);
|
database = AppDatabase.getDatabase(this);
|
||||||
databaseExecutor = Executors.newSingleThreadExecutor();
|
databaseExecutor = Executors.newSingleThreadExecutor();
|
||||||
chatAdapter = new ChatAdapter(new ArrayList<>()); // ChatAdapter 现在只显示当前会话的消息
|
chatAdapter = new ChatAdapter(new ArrayList<>());
|
||||||
recyclerViewChat.setLayoutManager(new LinearLayoutManager(this));
|
recyclerViewChat.setLayoutManager(new LinearLayoutManager(this));
|
||||||
recyclerViewChat.setAdapter(chatAdapter);
|
recyclerViewChat.setAdapter(chatAdapter);
|
||||||
|
|
||||||
// 旧的 sessionId 初始化,可以移除或让 startNewConversationSession 负责
|
loadChatHistory(); // This will start a new conversation session
|
||||||
// currentSessionId = generateNewSessionId();
|
|
||||||
// Log.d(TAG, "Old session ID logic: " + currentSessionId);
|
|
||||||
|
|
||||||
loadChatHistory(); // 这将启动一个新的会话
|
|
||||||
|
|
||||||
buttonSend.setOnClickListener(v -> sendMessage());
|
buttonSend.setOnClickListener(v -> sendMessage());
|
||||||
}
|
}
|
||||||
@ -102,21 +100,28 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
String newBaseUrlSetting = sharedPreferences.getString(SettingsActivity.KEY_API_BASE_URL, SettingsActivity.DEFAULT_API_BASE_URL);
|
String newBaseUrlSetting = sharedPreferences.getString(SettingsActivity.KEY_API_BASE_URL, SettingsActivity.DEFAULT_API_BASE_URL);
|
||||||
if (apiBaseUrl == null || !apiBaseUrl.equals(newBaseUrlSetting)) {
|
// Check if apiBaseUrl needs update or if apiService needs reinitialization
|
||||||
Log.d(TAG, "Base URL changed or was null. Old: " + apiBaseUrl + ", New: " + newBaseUrlSetting);
|
if (apiBaseUrl == null || !apiBaseUrl.equals(newBaseUrlSetting) || apiService == null) {
|
||||||
ApiClient.resetClient();
|
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() {
|
private void loadApiSettings() {
|
||||||
apiKey = sharedPreferences.getString(SettingsActivity.KEY_API_KEY, "");
|
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);
|
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) +
|
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 = "";
|
String placeholderApiKey = "";
|
||||||
try {
|
try {
|
||||||
@ -127,32 +132,38 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
if (apiKey.isEmpty() || apiKey.equals(placeholderApiKey)) {
|
if (apiKey.isEmpty() || apiKey.equals(placeholderApiKey)) {
|
||||||
Log.w(TAG, "API Key is not set or is placeholder.");
|
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() {
|
private void refreshConversation() {
|
||||||
Log.d(TAG, "refreshConversation called. Current active conversationId: " + currentConversationId);
|
Log.d(TAG, "refreshConversation called. Current active conversationId: " + currentConversationId);
|
||||||
saveCurrentConversationIfNeeded();
|
saveCurrentConversationIfNeeded(); // Save previous before starting new
|
||||||
startNewConversationSession();
|
startNewConversationSession();
|
||||||
|
|
||||||
chatAdapter.clearMessages(); // 清空UI
|
chatAdapter.clearMessages();
|
||||||
currentMessageListForSaving.clear(); // 清空内存中的消息列表
|
currentMessageListForSaving.clear();
|
||||||
|
|
||||||
Toast.makeText(this, "新对话已开始", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "新对话已开始", Toast.LENGTH_SHORT).show();
|
||||||
setLoadingState(false);
|
setLoadingState(false); // Ensure loading is off for new conversation
|
||||||
editTextMessage.setText("");
|
editTextMessage.setText("");
|
||||||
editTextMessage.requestFocus();
|
editTextMessage.requestFocus();
|
||||||
scrollToBottom(false); // 滚动到底部
|
scrollToBottom(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startNewConversationSession() {
|
private void startNewConversationSession() {
|
||||||
|
// Creates a new conversation entry in the DB and sets currentConversationId
|
||||||
Conversation newSessionConversation = new Conversation(System.currentTimeMillis(), "新对话开始...", System.currentTimeMillis());
|
Conversation newSessionConversation = new Conversation(System.currentTimeMillis(), "新对话开始...", System.currentTimeMillis());
|
||||||
databaseExecutor.execute(() -> {
|
databaseExecutor.execute(() -> {
|
||||||
currentConversationId = database.conversationDao().insertConversation(newSessionConversation);
|
currentConversationId = database.conversationDao().insertConversation(newSessionConversation);
|
||||||
Log.i(TAG, "New conversation session started with ID: " + currentConversationId);
|
Log.i(TAG, "New conversation session started with ID: " + currentConversationId);
|
||||||
mainThreadHandler.post(() -> {
|
mainThreadHandler.post(() -> {
|
||||||
currentMessageListForSaving.clear(); // 为新会话准备
|
currentMessageListForSaving.clear();
|
||||||
if(chatAdapter != null) chatAdapter.clearMessages(); // 确保UI也清空
|
if (chatAdapter != null) chatAdapter.clearMessages();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -160,14 +171,15 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private void saveCurrentConversationIfNeeded() {
|
private void saveCurrentConversationIfNeeded() {
|
||||||
if (currentConversationId != -1 && !currentMessageListForSaving.isEmpty()) {
|
if (currentConversationId != -1 && !currentMessageListForSaving.isEmpty()) {
|
||||||
Log.d(TAG, "Saving current conversation ID: " + currentConversationId + " with " + currentMessageListForSaving.size() + " messages.");
|
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;
|
final long conversationIdToSave = currentConversationId;
|
||||||
|
|
||||||
databaseExecutor.execute(() -> {
|
databaseExecutor.execute(() -> {
|
||||||
|
if (messagesToSave.isEmpty()) return; // Should not happen if check above is done
|
||||||
ChatMessage lastMessage = messagesToSave.get(messagesToSave.size() - 1);
|
ChatMessage lastMessage = messagesToSave.get(messagesToSave.size() - 1);
|
||||||
String preview = lastMessage.content;
|
String preview = lastMessage.content;
|
||||||
if (preview.length() > 50) {
|
if (preview.length() > 100) { // Increased preview length
|
||||||
preview = preview.substring(0, 50) + "...";
|
preview = preview.substring(0, 100) + "...";
|
||||||
}
|
}
|
||||||
Conversation conversationToUpdate = database.conversationDao().getConversationById(conversationIdToSave);
|
Conversation conversationToUpdate = database.conversationDao().getConversationById(conversationIdToSave);
|
||||||
if (conversationToUpdate != null) {
|
if (conversationToUpdate != null) {
|
||||||
@ -182,9 +194,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "No active conversation or no messages to save for convId: " + currentConversationId);
|
Log.d(TAG, "No active conversation or no messages to save for convId: " + currentConversationId);
|
||||||
if (currentConversationId != -1 && currentMessageListForSaving.isEmpty()) {
|
if (currentConversationId != -1 && currentMessageListForSaving.isEmpty()) {
|
||||||
|
// Optionally delete empty conversation shells if they are created but never used
|
||||||
final long emptyConvId = currentConversationId;
|
final long emptyConvId = currentConversationId;
|
||||||
databaseExecutor.execute(() -> {
|
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);
|
database.conversationDao().deleteConversationById(emptyConvId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -192,25 +205,111 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void openHistoryActivity() {
|
private void openHistoryActivity() {
|
||||||
// Toast.makeText(this, "历史对话功能待实现", Toast.LENGTH_SHORT).show();
|
|
||||||
Intent intent = new Intent(this, HistoryActivity.class);
|
Intent intent = new Intent(this, HistoryActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 旧的 generateNewSessionId() 可能不再需要,或者其逻辑并入 Conversation 管理
|
|
||||||
// private String generateNewSessionId() {
|
private List<ChatRequest.Message> buildApiContextMessages(List<ChatMessage> sourceMessages) {
|
||||||
// return UUID.randomUUID().toString();
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void sendMessage() {
|
private void sendMessage() {
|
||||||
loadApiSettings(); // 确保API设置是最新的
|
loadApiSettings(); // Ensure settings are current
|
||||||
|
|
||||||
if (currentConversationId == -1) {
|
if (currentConversationId == -1) {
|
||||||
Toast.makeText(this, "无法发送消息:当前会话无效", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "无法发送消息:当前会话无效", Toast.LENGTH_SHORT).show();
|
||||||
Log.e(TAG, "Cannot send message, currentConversationId is -1. Attempting to start a new session.");
|
Log.e(TAG, "Cannot send message, currentConversationId is -1. Attempting to start a new session.");
|
||||||
// 尝试强制开始一个新会话,并提示用户重试
|
|
||||||
startNewConversationSession();
|
startNewConversationSession();
|
||||||
// UI上给用户一些反馈,比如禁用发送按钮直到新会话ID获取成功
|
|
||||||
Toast.makeText(this, "正在初始化新会话,请稍后重试发送", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "正在初始化新会话,请稍后重试发送", Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -222,126 +321,115 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String placeholderApiKey = "";
|
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)) {
|
if (apiKey.isEmpty() || apiKey.equals(placeholderApiKey)) {
|
||||||
Toast.makeText(this, getString(R.string.error_api_key_not_set), Toast.LENGTH_LONG).show();
|
Toast.makeText(this, getString(R.string.error_api_key_not_set), Toast.LENGTH_LONG).show();
|
||||||
return;
|
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());
|
ChatMessage userMessage = new ChatMessage(currentConversationId, "user", messageText, System.currentTimeMillis());
|
||||||
|
// Save user message and update UI immediately.
|
||||||
|
// The saveMessageAndUpdateUi method adds to currentMessageListForSaving.
|
||||||
saveMessageAndUpdateUi(userMessage);
|
saveMessageAndUpdateUi(userMessage);
|
||||||
editTextMessage.setText("");
|
editTextMessage.setText(""); // Clear input field
|
||||||
setLoadingState(true);
|
|
||||||
|
|
||||||
List<ChatRequest.Message> apiContextMessages = new ArrayList<>();
|
// Build context messages *after* user message is added to currentMessageListForSaving
|
||||||
// 从 currentMessageListForSaving 构建上下文,确保包含最新的用户消息
|
List<ChatRequest.Message> apiContextMessages = buildApiContextMessages(new ArrayList<>(currentMessageListForSaving));
|
||||||
List<ChatMessage> contextSource = new ArrayList<>(currentMessageListForSaving); // currentMessageListForSaving 应已包含刚发送的userMessage
|
|
||||||
|
|
||||||
int startIndex = Math.max(0, contextSource.size() - MAX_CONTEXT_MESSAGES);
|
// Reset active call count before making new calls
|
||||||
for (int i = startIndex; i < contextSource.size(); i++) {
|
// This should be handled carefully if sendMessage can be called rapidly.
|
||||||
ChatMessage chatMsg = contextSource.get(i);
|
// For simplicity, assuming one "send operation" at a time.
|
||||||
apiContextMessages.add(new ChatRequest.Message(chatMsg.role, chatMsg.content));
|
// The increment/decrement logic handles the loading state.
|
||||||
}
|
|
||||||
// 如果 saveMessageAndUpdateUi 是完全异步的,并且 currentMessageListForSaving 可能还没更新,
|
|
||||||
// 则需要确保 userMessage 被显式添加到 apiContextMessages,但你的 saveMessageAndUpdateUi 看起来会同步更新 currentMessageListForSaving 的引用
|
|
||||||
// 为保险起见,可以检查最后一条是否是当前用户消息,如果不是则添加。
|
|
||||||
// 但更稳妥的方式是 saveMessageAndUpdateUi完成后再构建apiContextMessages,或者传递userMessage。
|
|
||||||
// 这里我们遵循你提供的逻辑,它在循环后又添加了一次。
|
|
||||||
// 但你的示例代码中,循环后添加userMessage.content 的部分被注释掉了,我将它恢复并确保它使用的是最新的列表
|
|
||||||
// 实际上,如果 saveMessageAndUpdateUi 立即将 userMessage 添加到 currentMessageListForSaving,
|
|
||||||
// 那么下面的循环就应该能取到它。为清晰起见,我们直接从 currentMessageListForSaving 构建。
|
|
||||||
|
|
||||||
if (apiContextMessages.isEmpty() && userMessage != null) { // 如果列表为空但我们确实有用户消息
|
boolean callMade = false;
|
||||||
Log.w(TAG, "API context was empty, adding current user message directly.");
|
|
||||||
apiContextMessages.add(new ChatRequest.Message(userMessage.role, userMessage.content));
|
// 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);
|
if (!callMade) {
|
||||||
String authHeader = "Bearer " + apiKey;
|
// This case should ideally not be reached if modelName1 is always set.
|
||||||
// apiService = ApiClient.getClient(this).create(DeepSeekApiService.class); // 确保apiService已初始化
|
handleApiError("没有配置任何模型,请在设置中选择模型。");
|
||||||
|
// No need to call setLoadingState(false) here, as it wasn't set to true
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveMessageAndUpdateUi(ChatMessage message) {
|
private void saveMessageAndUpdateUi(ChatMessage message) {
|
||||||
// 确保消息关联到当前会话
|
|
||||||
if (message.conversationId == -1 && currentConversationId != -1) {
|
if (message.conversationId == -1 && currentConversationId != -1) {
|
||||||
message.conversationId = currentConversationId;
|
message.conversationId = currentConversationId;
|
||||||
} else if (message.conversationId == -1 && currentConversationId == -1) {
|
} 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.");
|
Log.e(TAG, "Message cannot be saved. No active conversation session (convId: -1).");
|
||||||
Toast.makeText(this, "错误:无法保存消息,会话无效。正在尝试创建新会话...", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "错误:无法保存消息,会话无效。", Toast.LENGTH_LONG).show();
|
||||||
startNewConversationSession(); // 尝试启动,然后让用户重试发送
|
// Optionally, try to start a new session, but this might lead to lost messages if not handled carefully
|
||||||
return; // 阻止当前消息被处理,因为会话ID无效
|
// startNewConversationSession();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ChatMessage messageToSave = message;
|
final ChatMessage messageToSave = message;
|
||||||
|
|
||||||
databaseExecutor.execute(() -> {
|
databaseExecutor.execute(() -> {
|
||||||
long insertedMessageId = database.chatMessageDao().insert(messageToSave);
|
long insertedMessageId = database.chatMessageDao().insert(messageToSave);
|
||||||
messageToSave.id = (int) insertedMessageId;
|
messageToSave.id = (int) insertedMessageId; // Update the object with the DB-generated ID
|
||||||
|
|
||||||
mainThreadHandler.post(() -> {
|
mainThreadHandler.post(() -> {
|
||||||
if (chatAdapter != null) {
|
if (chatAdapter != null) {
|
||||||
chatAdapter.addMessage(messageToSave);
|
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);
|
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() {
|
private void loadChatHistory() {
|
||||||
Log.d(TAG, "loadChatHistory called - starting new conversation session for MainActivity.");
|
Log.d(TAG, "loadChatHistory called - starting new conversation session for MainActivity.");
|
||||||
startNewConversationSession(); // MainActivity 总是开始一个新对话
|
startNewConversationSession(); // MainActivity always starts a new conversation on create
|
||||||
setLoadingState(false);
|
setLoadingState(false); // Ensure loading is off initially
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleApiError(String errorMessage) {
|
private void handleApiError(String errorMessage) {
|
||||||
mainThreadHandler.post(() -> {
|
mainThreadHandler.post(() -> {
|
||||||
Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
|
Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
|
||||||
Log.e(TAG, errorMessage);
|
Log.e(TAG, "API_ERROR: " + errorMessage);
|
||||||
setLoadingState(false); // 确保在API错误时也解除加载状态
|
// 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) {
|
private void setLoadingState(boolean isLoading) {
|
||||||
|
Log.d(TAG, "Setting loading state to: " + isLoading);
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
buttonSend.setEnabled(false);
|
buttonSend.setEnabled(false);
|
||||||
@ -372,21 +461,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
MenuInflater inflater = getMenuInflater();
|
MenuInflater inflater = getMenuInflater();
|
||||||
inflater.inflate(R.menu.menu_main, menu);
|
inflater.inflate(R.menu.menu_main, menu);
|
||||||
// 根据新的逻辑,刷新按钮可以调用 refreshConversation()
|
// Removed direct listeners as they are handled in onOptionsItemSelected
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,19 +472,13 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
Intent intent = new Intent(this, SettingsActivity.class);
|
Intent intent = new Intent(this, SettingsActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
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);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,7 +492,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
Log.d(TAG, "onDestroy called. Saving current conversation if needed before shutdown.");
|
Log.d(TAG, "onDestroy called. Saving current conversation if needed before shutdown.");
|
||||||
saveCurrentConversationIfNeeded();
|
saveCurrentConversationIfNeeded(); // Ensure last save attempt
|
||||||
if (databaseExecutor != null && !databaseExecutor.isShutdown()) {
|
if (databaseExecutor != null && !databaseExecutor.isShutdown()) {
|
||||||
databaseExecutor.shutdown();
|
databaseExecutor.shutdown();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,9 @@ import android.os.Bundle;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.AutoCompleteTextView; // Import AutoCompleteTextView
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.Spinner;
|
// import android.widget.Spinner; // Spinner is no longer used
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
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 SHARED_PREFS_NAME = "AppSettings";
|
||||||
public static final String KEY_API_KEY = "apiKey";
|
public static final String KEY_API_KEY = "apiKey";
|
||||||
public static final String KEY_API_BASE_URL = "apiBaseUrl";
|
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/";
|
// New SharedPreferences Keys for two models
|
||||||
public static final String DEFAULT_MODEL = "deepseek-chat";
|
public static final String KEY_SELECTED_MODEL_1 = "selectedModel1";
|
||||||
public static final List<String> AVAILABLE_MODELS = Arrays.asList("deepseek-chat", "deepseek-coder", "deepseek-reasoner", "gpt-3.5-turbo", "gpt-4o-mini");
|
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 editTextApiKey;
|
||||||
private TextInputEditText editTextApiBaseUrl;
|
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 Button buttonSaveSettings;
|
||||||
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
@ -41,104 +58,114 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(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) {
|
if (getSupportActionBar() != null) {
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // For the back button
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
getSupportActionBar().setDisplayShowHomeEnabled(true); // For the back button
|
|
||||||
// getSupportActionBar().setTitle(getString(R.string.title_activity_settings)); // Optional: Set title if needed
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
// --- 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);
|
sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
|
|
||||||
editTextApiKey = findViewById(R.id.editTextApiKey);
|
editTextApiKey = findViewById(R.id.editTextApiKey);
|
||||||
editTextApiBaseUrl = findViewById(R.id.editTextApiBaseUrl);
|
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);
|
buttonSaveSettings = findViewById(R.id.buttonSaveSettings);
|
||||||
|
|
||||||
setupSpinner();
|
// setupSpinner(); // Old spinner setup is no longer needed
|
||||||
|
setupAutoCompleteModelViews(); // New setup for AutoCompleteTextViews
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|
||||||
buttonSaveSettings.setOnClickListener(v -> saveSettings());
|
buttonSaveSettings.setOnClickListener(v -> saveSettings());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSpinner() {
|
// New method to set up both AutoCompleteTextViews
|
||||||
|
private void setupAutoCompleteModelViews() {
|
||||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
||||||
android.R.layout.simple_spinner_item, AVAILABLE_MODELS);
|
android.R.layout.simple_dropdown_item_1line, AVAILABLE_MODELS);
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
autoCompleteTextViewModel1.setAdapter(adapter);
|
||||||
spinnerModel.setAdapter(adapter);
|
autoCompleteTextViewModel2.setAdapter(adapter); // Both can share the same adapter and list
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadSettings() {
|
private void loadSettings() {
|
||||||
String apiKey = sharedPreferences.getString(KEY_API_KEY, "");
|
String apiKey = sharedPreferences.getString(KEY_API_KEY, "");
|
||||||
String apiBaseUrl = sharedPreferences.getString(KEY_API_BASE_URL, DEFAULT_API_BASE_URL);
|
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);
|
editTextApiKey.setText(apiKey);
|
||||||
editTextApiBaseUrl.setText(apiBaseUrl);
|
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);
|
int modelPosition = AVAILABLE_MODELS.indexOf(selectedModel);
|
||||||
if (modelPosition >= 0) {
|
if (modelPosition >= 0) {
|
||||||
spinnerModel.setSelection(modelPosition);
|
spinnerModel.setSelection(modelPosition);
|
||||||
} else if (!AVAILABLE_MODELS.isEmpty()) {
|
} else if (!AVAILABLE_MODELS.isEmpty()) {
|
||||||
spinnerModel.setSelection(0);
|
spinnerModel.setSelection(0);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveSettings() {
|
private void saveSettings() {
|
||||||
String apiKey = editTextApiKey.getText() != null ? editTextApiKey.getText().toString().trim() : "";
|
String apiKey = editTextApiKey.getText() != null ? editTextApiKey.getText().toString().trim() : "";
|
||||||
String apiBaseUrl = editTextApiBaseUrl.getText() != null ? editTextApiBaseUrl.getText().toString().trim() : DEFAULT_API_BASE_URL;
|
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)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(apiBaseUrl)) {
|
if (TextUtils.isEmpty(apiBaseUrl)) {
|
||||||
apiBaseUrl = DEFAULT_API_BASE_URL;
|
apiBaseUrl = DEFAULT_API_BASE_URL; // Fallback to default if empty
|
||||||
editTextApiBaseUrl.setText(apiBaseUrl);
|
editTextApiBaseUrl.setText(apiBaseUrl);
|
||||||
}
|
}
|
||||||
|
// Ensure base URL ends with a slash
|
||||||
if (!apiBaseUrl.endsWith("/")) {
|
if (!apiBaseUrl.endsWith("/")) {
|
||||||
apiBaseUrl += "/";
|
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();
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||||
editor.putString(KEY_API_KEY, apiKey);
|
editor.putString(KEY_API_KEY, apiKey);
|
||||||
editor.putString(KEY_API_BASE_URL, apiBaseUrl);
|
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();
|
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();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
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) {
|
if (item.getItemId() == android.R.id.home) {
|
||||||
finish(); // Go back to the previous activity
|
finish();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.lotus_lab.jetchat.ui;
|
package com.lotus_lab.jetchat.ui;
|
||||||
|
|
||||||
import android.util.Log; // Import Log
|
import android.util.Log; // 导入 Log 类
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@ -12,17 +12,18 @@ import androidx.core.content.ContextCompat;
|
|||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.lotus_lab.jetchat.R;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.MessageViewHolder> {
|
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;
|
private final List<ChatMessage> messages;
|
||||||
|
|
||||||
// 在 ChatMessage 类中定义这些常量,或者在这里定义,以避免硬编码字符串 "assistant" 和 "user"
|
// 可以在 ChatMessage 类中定义这些常量,或者在这里定义,以避免硬编码字符串 "assistant" 和 "user"
|
||||||
// public static final String ROLE_USER = "user";
|
// public static final String ROLE_USER = "user";
|
||||||
// public static final String ROLE_ASSISTANT = "assistant";
|
// public static final String ROLE_ASSISTANT = "assistant";
|
||||||
|
|
||||||
@ -34,38 +35,38 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.MessageViewHol
|
|||||||
int oldSize = messages.size();
|
int oldSize = messages.size();
|
||||||
this.messages.clear();
|
this.messages.clear();
|
||||||
notifyItemRangeRemoved(0, oldSize);
|
notifyItemRangeRemoved(0, oldSize);
|
||||||
Log.d(ADAPTER_TAG, "Messages cleared. Old size: " + oldSize);
|
Log.d(ADAPTER_TAG, "消息已清空。旧数量: " + oldSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ChatMessage> getMessages() {
|
public List<ChatMessage> getMessages() {
|
||||||
return new ArrayList<>(this.messages); // Return a copy
|
return new ArrayList<>(this.messages); // 返回列表的副本
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMessages(List<ChatMessage> newMessages) {
|
public void setMessages(List<ChatMessage> newMessages) {
|
||||||
this.messages.clear();
|
this.messages.clear();
|
||||||
if (newMessages != null) { // Add null check for safety
|
if (newMessages != null) { // 为安全起见,添加 null 检查
|
||||||
this.messages.addAll(newMessages);
|
this.messages.addAll(newMessages);
|
||||||
}
|
}
|
||||||
notifyDataSetChanged(); // Consider using DiffUtil for better performance in production
|
notifyDataSetChanged(); // 在生产环境中,考虑使用 DiffUtil 以获得更好的性能
|
||||||
Log.d(ADAPTER_TAG, "Messages set. New count: " + this.messages.size());
|
Log.d(ADAPTER_TAG, "消息已设置。新数量: " + this.messages.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addMessage(ChatMessage message) {
|
public void addMessage(ChatMessage message) {
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
Log.w(ADAPTER_TAG, "Attempted to add a null message.");
|
Log.w(ADAPTER_TAG, "尝试添加一个 null 消息。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.messages.add(message);
|
this.messages.add(message);
|
||||||
notifyItemInserted(messages.size() - 1);
|
notifyItemInserted(messages.size() - 1);
|
||||||
Log.d(ADAPTER_TAG, "Message added. Role: " + (message.role != null ? message.role : "null") +
|
Log.d(ADAPTER_TAG, "消息已添加。角色: " + (message.role != null ? message.role : "null") +
|
||||||
", Content: '" + (message.content != null ? message.content : "null") +
|
", 内容: '" + (message.content != null ? message.content.substring(0, Math.min(message.content.length(), 50))+"..." : "null") + // 截断过长的内容日志
|
||||||
"'. New count: " + messages.size());
|
"'. 新数量: " + messages.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public MessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
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())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_message, parent, false);
|
.inflate(R.layout.item_message, parent, false);
|
||||||
return new MessageViewHolder(view);
|
return new MessageViewHolder(view);
|
||||||
@ -76,73 +77,74 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.MessageViewHol
|
|||||||
ChatMessage message = messages.get(position);
|
ChatMessage message = messages.get(position);
|
||||||
|
|
||||||
Log.i(ADAPTER_TAG, "-----------------------------------------------------");
|
Log.i(ADAPTER_TAG, "-----------------------------------------------------");
|
||||||
Log.i(ADAPTER_TAG, "onBindViewHolder - Position: " + position);
|
Log.i(ADAPTER_TAG, "onBindViewHolder - 位置: " + position);
|
||||||
|
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
Log.e(ADAPTER_TAG, "Message object is NULL at position " + position + ". Hiding item.");
|
Log.e(ADAPTER_TAG, "位置 " + position + " 的 Message 对象为 NULL。正在隐藏项目。");
|
||||||
holder.itemView.setVisibility(View.GONE); // Hide the item if message is null
|
holder.itemView.setVisibility(View.GONE); // 如果消息为 null,则隐藏该项目
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the raw role and content from the ChatMessage object
|
// 记录 ChatMessage 对象的原始角色和内容
|
||||||
Log.d(ADAPTER_TAG, "Raw Message Role: '" + message.role + "'");
|
Log.d(ADAPTER_TAG, "原始消息角色: '" + message.role + "'");
|
||||||
Log.d(ADAPTER_TAG, "Raw Message Content: '" + message.content + "'");
|
Log.d(ADAPTER_TAG, "原始消息内容: '" + message.content + "'");
|
||||||
|
|
||||||
|
|
||||||
if (message.role == null || message.content == null) {
|
if (message.role == null || message.content == null) {
|
||||||
holder.itemView.setVisibility(View.GONE);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure item is visible if data is valid
|
// 如果数据有效,确保项目可见
|
||||||
holder.itemView.setVisibility(View.VISIBLE);
|
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);
|
// holder.itemView.setBackground(null);
|
||||||
|
|
||||||
|
|
||||||
// Crucial check: compare role with "assistant" and "user"
|
// 关键检查:将角色与 "assistant" 和 "user" 进行比较
|
||||||
// Make sure these strings EXACTLY match what's in your ChatMessage.role
|
// 确保这些字符串与 ChatMessage.role 中的内容完全匹配
|
||||||
boolean isAiMessage = "assistant".equals(message.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.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,
|
// 将文本颜色和背景重置为 XML 或基本样式中的默认值,
|
||||||
// in case previous debug styles are cached by RecyclerView's ViewHolder.
|
// 以防 RecyclerView 的 ViewHolder 缓存了之前的调试样式。
|
||||||
holder.tvMessage.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.black)); // Or your R.color.your_default_text_color
|
// holder.tvMessage.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.black)); // 或者你的 R.color.your_default_text_color
|
||||||
// holder.tvMessage.setBackground(null); // Let specific backgrounds below take precedence
|
// holder.tvMessage.setBackground(null); // 让下面的特定背景优先
|
||||||
|
|
||||||
|
|
||||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) holder.tvMessage.getLayoutParams();
|
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) holder.tvMessage.getLayoutParams();
|
||||||
|
|
||||||
if (isAiMessage) {
|
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.ivAiAvatar.setVisibility(View.VISIBLE);
|
||||||
holder.ivUserAvatar.setVisibility(View.GONE);
|
holder.ivUserAvatar.setVisibility(View.GONE);
|
||||||
params.horizontalBias = 0.0f; // AI 消息靠左
|
params.horizontalBias = 0.0f; // AI 消息靠左
|
||||||
holder.tvMessage.setBackground(
|
holder.tvMessage.setBackground(
|
||||||
ContextCompat.getDrawable(holder.itemView.getContext(), R.drawable.bg_reply_message_bubble)
|
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));
|
// 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
|
} else { // 目前假设任何非 AI 消息都是用户消息
|
||||||
// If you have other roles like "system", you'll need more specific checks.
|
// 如果有其他角色(如 "system"),则需要更具体的检查。
|
||||||
Log.d(ADAPTER_TAG, "Applying User message style for: '" + message.content.substring(0, Math.min(message.content.length(), 20)) + "...'");
|
Log.d(ADAPTER_TAG, "正在为用户消息应用样式: '" + message.content.substring(0, Math.min(message.content.length(), 20)) + "...'");
|
||||||
holder.ivAiAvatar.setVisibility(View.GONE);
|
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; // 用户消息靠右
|
params.horizontalBias = 1.0f; // 用户消息靠右
|
||||||
holder.tvMessage.setBackground(
|
holder.tvMessage.setBackground(
|
||||||
ContextCompat.getDrawable(holder.itemView.getContext(), R.drawable.bg_user_message_bubble)
|
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.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.user_text_color));
|
||||||
}
|
}
|
||||||
holder.tvMessage.setLayoutParams(params);
|
holder.tvMessage.setLayoutParams(params);
|
||||||
Log.i(ADAPTER_TAG, "Finished applying style for position: " + position);
|
Log.i(ADAPTER_TAG, "已完成为位置 " + position + " 应用样式");
|
||||||
Log.i(ADAPTER_TAG, "-----------------------------------------------------");
|
Log.i(ADAPTER_TAG, "-----------------------------------------------------");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,12 +161,13 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.MessageViewHol
|
|||||||
MessageViewHolder(@NonNull View itemView) {
|
MessageViewHolder(@NonNull View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
ivAiAvatar = itemView.findViewById(R.id.iv_ai_avatar);
|
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);
|
tvMessage = itemView.findViewById(R.id.tv_message);
|
||||||
|
|
||||||
if (ivAiAvatar == null) Log.e(ADAPTER_TAG, "MessageViewHolder: ivAiAvatar is null!");
|
if (ivAiAvatar == null) Log.e(ADAPTER_TAG, "MessageViewHolder: ivAiAvatar 为 null!");
|
||||||
if (ivUserAvatar == null) Log.e(ADAPTER_TAG, "MessageViewHolder: ivUserAvatar is null!");
|
// ivUserAvatar 在你的布局中可能确实不存在或ID不同,如果它总是GONE,可以不查找或处理null
|
||||||
if (tvMessage == null) Log.e(ADAPTER_TAG, "MessageViewHolder: tvMessage is 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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone"
|
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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:visibility="visible"/>
|
tools:visibility="visible"/>
|
||||||
|
|||||||
@ -54,19 +54,51 @@
|
|||||||
android:inputType="textUri" />
|
android:inputType="textUri" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- 模型 1 选择 -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/label_select_model"
|
android:text="模型 1 (主要)"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:layout_marginBottom="8dp"/>
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
<Spinner
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/spinnerModel"
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="24dp"
|
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
|
<Button
|
||||||
android:id="@+id/buttonSaveSettings"
|
android:id="@+id/buttonSaveSettings"
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
android:maxWidth="280dp"
|
android:maxWidth="280dp"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:textColor="@android:color/black"
|
android:textColor="@android:color/black"
|
||||||
|
android:textIsSelectable="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/iv_user_avatar"
|
app:layout_constraintEnd_toStartOf="@+id/iv_user_avatar"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
|||||||
@ -7,12 +7,13 @@
|
|||||||
<string name="settings_title">设置</string>
|
<string name="settings_title">设置</string>
|
||||||
<string name="hint_api_key">API 密钥</string>
|
<string name="hint_api_key">API 密钥</string>
|
||||||
<string name="hint_api_base_url">API 基础 URL (例如 https://api.example.com/)</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="button_save_settings">保存设置</string>
|
||||||
<string name="toast_settings_saved">设置已保存</string>
|
<string name="toast_settings_saved">设置已保存</string>
|
||||||
<string name="toast_api_key_required">API 密钥不能为空</string>
|
<string name="toast_api_key_required">API 密钥不能为空</string>
|
||||||
<string name="menu_settings">设置</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_model_not_set">请在设置中选择一个模型</string>
|
||||||
<string name="error_base_url_not_set">请在设置中配置API基础URL</string>
|
<string name="error_base_url_not_set">请在设置中配置API基础URL</string>
|
||||||
|
|
||||||
@ -24,5 +25,13 @@
|
|||||||
<string name="menu_refresh_conversation">刷新对话</string>
|
<string name="menu_refresh_conversation">刷新对话</string>
|
||||||
<string name="menu_history">对话历史</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>
|
</resources>
|
||||||
Loading…
Reference in New Issue
Block a user