From 4ed7b82139175a1e1ec9bbdb53b4ec64426cb762 Mon Sep 17 00:00:00 2001 From: renjianbo <263303411@qq.com> Date: Wed, 4 Mar 2026 22:33:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=81=A2=E5=A4=8D=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E7=9B=B4=E7=AD=94=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=9B=B4=E7=AD=94?= =?UTF-8?q?=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruilaizi/example/HistoryActivity.java | 35 ++++-- .../com/ruilaizi/example/MainActivity.java | 6 + .../adapter/DirectAnswerHistoryAdapter.java | 107 ++++++++++++++++++ .../ruilaizi/example/data/db/AppDatabase.java | 3 +- .../example/data/db/DirectAnswerRecord.java | 31 +++++ .../data/db/DirectAnswerRecordDao.java | 21 ++++ .../data/repository/GenerationRepository.java | 15 +++ .../ruilaizi/example/ui/HistoryViewModel.java | 5 + .../ruilaizi/example/ui/MainViewModel.java | 44 +++++++ .../src/main/res/layout/activity_history.xml | 18 +++ .../app/src/main/res/layout/activity_main.xml | 15 ++- .../res/layout/item_direct_answer_history.xml | 67 +++++++++++ example/app/src/main/res/values/strings.xml | 7 +- 13 files changed, 358 insertions(+), 16 deletions(-) create mode 100644 example/app/src/main/java/com/ruilaizi/example/adapter/DirectAnswerHistoryAdapter.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/db/DirectAnswerRecord.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/db/DirectAnswerRecordDao.java create mode 100644 example/app/src/main/res/layout/item_direct_answer_history.xml diff --git a/example/app/src/main/java/com/ruilaizi/example/HistoryActivity.java b/example/app/src/main/java/com/ruilaizi/example/HistoryActivity.java index c36a3a6..d2217d6 100644 --- a/example/app/src/main/java/com/ruilaizi/example/HistoryActivity.java +++ b/example/app/src/main/java/com/ruilaizi/example/HistoryActivity.java @@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.google.android.material.tabs.TabLayout; +import com.ruilaizi.example.adapter.DirectAnswerHistoryAdapter; import com.ruilaizi.example.adapter.GenerationHistoryAdapter; import com.ruilaizi.example.adapter.OptimizeHistoryAdapter; import com.ruilaizi.example.databinding.ActivityHistoryBinding; @@ -20,7 +21,7 @@ import com.ruilaizi.example.ui.HistoryViewModel; /** - * 生成历史 / 优化历史 页面。 + * 生成历史 / 优化历史 / 直答历史 页面。 */ public class HistoryActivity extends AppCompatActivity { @@ -30,6 +31,7 @@ public class HistoryActivity extends AppCompatActivity { private HistoryViewModel viewModel; private OptimizeHistoryAdapter optimizeAdapter; private GenerationHistoryAdapter generationAdapter; + private DirectAnswerHistoryAdapter directAnswerAdapter; private int currentTab = 0; @Override @@ -63,16 +65,28 @@ public class HistoryActivity extends AppCompatActivity { finish(); }); + directAnswerAdapter = new DirectAnswerHistoryAdapter(); + directAnswerAdapter.setOnUseDirectAnswerListener(question -> { + Intent data = new Intent(); + data.putExtra(EXTRA_USE_PROMPT, question); + setResult(RESULT_OK, data); + finish(); + }); + binding.recyclerOptimize.setLayoutManager(new LinearLayoutManager(this)); binding.recyclerOptimize.setAdapter(optimizeAdapter); binding.recyclerGeneration.setLayoutManager(new LinearLayoutManager(this)); binding.recyclerGeneration.setAdapter(generationAdapter); + + binding.recyclerDirectAnswer.setLayoutManager(new LinearLayoutManager(this)); + binding.recyclerDirectAnswer.setAdapter(directAnswerAdapter); } private void setupTabs() { binding.tabHistory.addTab(binding.tabHistory.newTab().setText(R.string.history_tab_optimize)); binding.tabHistory.addTab(binding.tabHistory.newTab().setText(R.string.history_tab_generation)); + binding.tabHistory.addTab(binding.tabHistory.newTab().setText(R.string.history_tab_direct_answer)); binding.tabHistory.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override @@ -90,15 +104,10 @@ public class HistoryActivity extends AppCompatActivity { private void switchTab(int index) { currentTab = index; - if (index == 0) { - binding.recyclerOptimize.setVisibility(View.VISIBLE); - binding.recyclerGeneration.setVisibility(View.GONE); - updateEmptyVisibility(); - } else { - binding.recyclerOptimize.setVisibility(View.GONE); - binding.recyclerGeneration.setVisibility(View.VISIBLE); - updateEmptyVisibility(); - } + binding.recyclerOptimize.setVisibility(index == 0 ? View.VISIBLE : View.GONE); + binding.recyclerGeneration.setVisibility(index == 1 ? View.VISIBLE : View.GONE); + binding.recyclerDirectAnswer.setVisibility(index == 2 ? View.VISIBLE : View.GONE); + updateEmptyVisibility(); } private void setupBack() { @@ -114,12 +123,18 @@ public class HistoryActivity extends AppCompatActivity { generationAdapter.setData(list); updateEmptyVisibility(); }); + viewModel.getDirectAnswerRecords().observe(this, list -> { + directAnswerAdapter.setData(list); + updateEmptyVisibility(); + }); } private void updateEmptyVisibility() { boolean optEmpty = optimizeAdapter.getItemCount() == 0; boolean genEmpty = generationAdapter.getItemCount() == 0; + boolean directEmpty = directAnswerAdapter.getItemCount() == 0; binding.emptyOptimize.setVisibility(currentTab == 0 && optEmpty ? View.VISIBLE : View.GONE); binding.emptyGeneration.setVisibility(currentTab == 1 && genEmpty ? View.VISIBLE : View.GONE); + binding.emptyDirectAnswer.setVisibility(currentTab == 2 && directEmpty ? View.VISIBLE : View.GONE); } } diff --git a/example/app/src/main/java/com/ruilaizi/example/MainActivity.java b/example/app/src/main/java/com/ruilaizi/example/MainActivity.java index 8f14501..1198d9a 100644 --- a/example/app/src/main/java/com/ruilaizi/example/MainActivity.java +++ b/example/app/src/main/java/com/ruilaizi/example/MainActivity.java @@ -115,6 +115,10 @@ public class MainActivity extends AppCompatActivity { if (btnOptimize != null) { btnOptimize.setOnClickListener(v -> viewModel.optimizePrompt()); } + MaterialButton btnDirectAnswer = binding.btnDirectAnswer; + if (btnDirectAnswer != null) { + btnDirectAnswer.setOnClickListener(v -> viewModel.directAnswer()); + } } private void setupResultAndActions() { @@ -144,9 +148,11 @@ public class MainActivity extends AppCompatActivity { ProgressBar progress = binding.progressBar; MaterialButton btn = binding.btnGenerate; MaterialButton btnOpt = binding.btnOptimize; + MaterialButton btnDirect = binding.btnDirectAnswer; if (progress != null) progress.setVisibility(loading != null && loading ? View.VISIBLE : View.GONE); if (btn != null) btn.setEnabled(loading == null || !loading); if (btnOpt != null) btnOpt.setEnabled(loading == null || !loading); + if (btnDirect != null) btnDirect.setEnabled(loading == null || !loading); }); viewModel.getErrorLiveData().observe(this, error -> { diff --git a/example/app/src/main/java/com/ruilaizi/example/adapter/DirectAnswerHistoryAdapter.java b/example/app/src/main/java/com/ruilaizi/example/adapter/DirectAnswerHistoryAdapter.java new file mode 100644 index 0000000..db7aa02 --- /dev/null +++ b/example/app/src/main/java/com/ruilaizi/example/adapter/DirectAnswerHistoryAdapter.java @@ -0,0 +1,107 @@ +package com.ruilaizi.example.adapter; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.button.MaterialButton; +import com.ruilaizi.example.R; +import com.ruilaizi.example.data.db.DirectAnswerRecord; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class DirectAnswerHistoryAdapter extends RecyclerView.Adapter { + + private final List list = new ArrayList<>(); + private OnUseDirectAnswerListener onUseDirectAnswerListener; + + public interface OnUseDirectAnswerListener { + void onUseQuestion(String question); + } + + public void setOnUseDirectAnswerListener(OnUseDirectAnswerListener listener) { + this.onUseDirectAnswerListener = listener; + } + + public void setData(List data) { + list.clear(); + if (data != null) { + list.addAll(data); + } + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_direct_answer_history, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + DirectAnswerRecord r = list.get(position); + holder.tvQuestion.setText("问:" + truncate(r.question, 80)); + holder.tvAnswer.setText("答:" + truncate(r.answer, 150)); + holder.tvTime.setText(formatTime(r.createdAt)); + + holder.btnUse.setOnClickListener(v -> { + if (onUseDirectAnswerListener != null && r.question != null) { + onUseDirectAnswerListener.onUseQuestion(r.question); + } + }); + + holder.btnCopy.setOnClickListener(v -> { + if (r.answer != null && !r.answer.isEmpty()) { + ClipboardManager cm = (ClipboardManager) holder.itemView.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + if (cm != null) { + cm.setPrimaryClip(ClipData.newPlainText("直答", r.answer)); + } + } + }); + } + + @Override + public int getItemCount() { + return list.size(); + } + + private static String truncate(String s, int maxLen) { + if (s == null) return ""; + if (s.length() <= maxLen) return s; + return s.substring(0, maxLen) + "…"; + } + + private static String formatTime(long timestamp) { + try { + return new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(new Date(timestamp)); + } catch (Exception e) { + return ""; + } + } + + static class ViewHolder extends RecyclerView.ViewHolder { + TextView tvQuestion, tvAnswer, tvTime; + MaterialButton btnUse, btnCopy; + + ViewHolder(View itemView) { + super(itemView); + tvQuestion = itemView.findViewById(R.id.tv_question); + tvAnswer = itemView.findViewById(R.id.tv_answer); + tvTime = itemView.findViewById(R.id.tv_time); + btnUse = itemView.findViewById(R.id.btn_use); + btnCopy = itemView.findViewById(R.id.btn_copy); + } + } +} diff --git a/example/app/src/main/java/com/ruilaizi/example/data/db/AppDatabase.java b/example/app/src/main/java/com/ruilaizi/example/data/db/AppDatabase.java index 3dcac68..34e604b 100644 --- a/example/app/src/main/java/com/ruilaizi/example/data/db/AppDatabase.java +++ b/example/app/src/main/java/com/ruilaizi/example/data/db/AppDatabase.java @@ -6,7 +6,7 @@ import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; -@Database(entities = {GenerationRecord.class, OptimizeRecord.class}, version = 2, exportSchema = false) +@Database(entities = {GenerationRecord.class, OptimizeRecord.class, DirectAnswerRecord.class}, version = 3, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { private static volatile AppDatabase INSTANCE; @@ -28,4 +28,5 @@ public abstract class AppDatabase extends RoomDatabase { public abstract GenerationRecordDao generationRecordDao(); public abstract OptimizeRecordDao optimizeRecordDao(); + public abstract DirectAnswerRecordDao directAnswerRecordDao(); } diff --git a/example/app/src/main/java/com/ruilaizi/example/data/db/DirectAnswerRecord.java b/example/app/src/main/java/com/ruilaizi/example/data/db/DirectAnswerRecord.java new file mode 100644 index 0000000..ce3df92 --- /dev/null +++ b/example/app/src/main/java/com/ruilaizi/example/data/db/DirectAnswerRecord.java @@ -0,0 +1,31 @@ +package com.ruilaizi.example.data.db; + +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.PrimaryKey; + +/** + * 模型直答记录:用户提问 + 模型回答。 + */ +@Entity(tableName = "direct_answer_records") +public class DirectAnswerRecord { + + @PrimaryKey(autoGenerate = true) + public long id; + + /** 用户提问 */ + public String question; + /** 模型回答 */ + public String answer; + /** 创建时间戳 */ + public long createdAt; + + public DirectAnswerRecord() {} + + @Ignore + public DirectAnswerRecord(String question, String answer) { + this.question = question; + this.answer = answer; + this.createdAt = System.currentTimeMillis(); + } +} diff --git a/example/app/src/main/java/com/ruilaizi/example/data/db/DirectAnswerRecordDao.java b/example/app/src/main/java/com/ruilaizi/example/data/db/DirectAnswerRecordDao.java new file mode 100644 index 0000000..5007c94 --- /dev/null +++ b/example/app/src/main/java/com/ruilaizi/example/data/db/DirectAnswerRecordDao.java @@ -0,0 +1,21 @@ +package com.ruilaizi.example.data.db; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.Query; + +import java.util.List; + +@Dao +public interface DirectAnswerRecordDao { + + @Insert + long insert(DirectAnswerRecord record); + + @Query("SELECT * FROM direct_answer_records ORDER BY createdAt DESC LIMIT 50") + LiveData> getRecentRecords(); + + @Query("DELETE FROM direct_answer_records WHERE id NOT IN (SELECT id FROM direct_answer_records ORDER BY createdAt DESC LIMIT 50)") + void keepOnlyRecent(); +} diff --git a/example/app/src/main/java/com/ruilaizi/example/data/repository/GenerationRepository.java b/example/app/src/main/java/com/ruilaizi/example/data/repository/GenerationRepository.java index 348dd35..ef86a76 100644 --- a/example/app/src/main/java/com/ruilaizi/example/data/repository/GenerationRepository.java +++ b/example/app/src/main/java/com/ruilaizi/example/data/repository/GenerationRepository.java @@ -10,6 +10,8 @@ import com.ruilaizi.example.data.api.OpenAICompletionService; import com.ruilaizi.example.data.api.StreamCallback; import com.ruilaizi.example.data.db.AppDatabase; import com.ruilaizi.example.data.db.GenerationRecord; +import com.ruilaizi.example.data.db.DirectAnswerRecord; +import com.ruilaizi.example.data.db.DirectAnswerRecordDao; import com.ruilaizi.example.data.db.GenerationRecordDao; import com.ruilaizi.example.data.db.OptimizeRecord; import com.ruilaizi.example.data.db.OptimizeRecordDao; @@ -24,6 +26,7 @@ public class GenerationRepository { private final ApiKeyProvider apiKeyProvider; private final GenerationRecordDao generationDao; private final OptimizeRecordDao optimizeDao; + private final DirectAnswerRecordDao directAnswerDao; private final PromptGenerator promptGenerator; public GenerationRepository(Context context) { @@ -31,6 +34,7 @@ public class GenerationRepository { aiService = new com.ruilaizi.example.data.api.OpenAIStreamService(null); generationDao = AppDatabase.getInstance(context).generationRecordDao(); optimizeDao = AppDatabase.getInstance(context).optimizeRecordDao(); + directAnswerDao = AppDatabase.getInstance(context).directAnswerRecordDao(); promptGenerator = new PromptGenerator(new OpenAICompletionService(null), apiKeyProvider); } @@ -92,10 +96,21 @@ public class GenerationRepository { return generationDao.getRecentRecords(); } + public void saveDirectAnswerRecord(DirectAnswerRecord record) { + new Thread(() -> { + directAnswerDao.insert(record); + directAnswerDao.keepOnlyRecent(); + }).start(); + } + public LiveData> getRecentOptimizeRecords() { return optimizeDao.getRecentRecords(); } + public LiveData> getRecentDirectAnswerRecords() { + return directAnswerDao.getRecentRecords(); + } + /** * 智能提示词优化(两阶段专家逻辑)。需在后台线程调用。 */ diff --git a/example/app/src/main/java/com/ruilaizi/example/ui/HistoryViewModel.java b/example/app/src/main/java/com/ruilaizi/example/ui/HistoryViewModel.java index 0db7f90..b9c0602 100644 --- a/example/app/src/main/java/com/ruilaizi/example/ui/HistoryViewModel.java +++ b/example/app/src/main/java/com/ruilaizi/example/ui/HistoryViewModel.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import com.ruilaizi.example.data.db.DirectAnswerRecord; import com.ruilaizi.example.data.db.GenerationRecord; import com.ruilaizi.example.data.db.OptimizeRecord; import com.ruilaizi.example.data.repository.GenerationRepository; @@ -31,4 +32,8 @@ public class HistoryViewModel extends AndroidViewModel { public LiveData> getGenerationRecords() { return repository.getRecentRecords(); } + + public LiveData> getDirectAnswerRecords() { + return repository.getRecentDirectAnswerRecords(); + } } diff --git a/example/app/src/main/java/com/ruilaizi/example/ui/MainViewModel.java b/example/app/src/main/java/com/ruilaizi/example/ui/MainViewModel.java index 0a3d11e..e3d2a35 100644 --- a/example/app/src/main/java/com/ruilaizi/example/ui/MainViewModel.java +++ b/example/app/src/main/java/com/ruilaizi/example/ui/MainViewModel.java @@ -11,6 +11,7 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.ruilaizi.example.data.api.StreamCallback; +import com.ruilaizi.example.data.db.DirectAnswerRecord; import com.ruilaizi.example.data.db.GenerationRecord; import com.ruilaizi.example.data.db.OptimizeRecord; import com.ruilaizi.example.data.prompt.PromptGenerator; @@ -154,6 +155,49 @@ public class MainViewModel extends AndroidViewModel { }); } + /** 模型直答:直接以用户输入提问,流式返回回答并保存历史 */ + public void directAnswer() { + String question = promptLiveData.getValue(); + if (question == null) question = ""; + question = question.trim(); + if (question.isEmpty()) { + snackbarLiveData.setValue("请输入问题"); + return; + } + if (repository.getApiKey() == null || repository.getApiKey().isEmpty()) { + errorLiveData.setValue("请先在设置中配置 API Key,或使用后端网关"); + return; + } + streamingBuffer.setLength(0); + resultLiveData.setValue(""); + loadingLiveData.setValue(true); + errorLiveData.setValue(null); + final String questionForRecord = question; + repository.requestStream(question, 1, 1, new StreamCallback() { + @Override + public void onChunk(String textChunk) { + streamingBuffer.append(textChunk); + resultLiveData.postValue(streamingBuffer.toString()); + } + + @Override + public void onComplete() { + loadingLiveData.postValue(false); + String full = streamingBuffer.toString(); + if (full.length() > 0) { + repository.saveDirectAnswerRecord(new DirectAnswerRecord(questionForRecord, full)); + } + snackbarLiveData.postValue("回答完成"); + } + + @Override + public void onError(Throwable t) { + loadingLiveData.postValue(false); + errorLiveData.postValue(t != null ? t.getMessage() : "直答失败"); + } + }); + } + /** 重新生成:用上次的 prompt 和参数再调一次 */ public void regenerate() { if (lastPrompt != null && !lastPrompt.isEmpty()) { diff --git a/example/app/src/main/res/layout/activity_history.xml b/example/app/src/main/res/layout/activity_history.xml index 8f21231..638a6db 100644 --- a/example/app/src/main/res/layout/activity_history.xml +++ b/example/app/src/main/res/layout/activity_history.xml @@ -67,6 +67,15 @@ android:clipToPadding="false" tools:listitem="@layout/item_generation_history" /> + + + + diff --git a/example/app/src/main/res/layout/activity_main.xml b/example/app/src/main/res/layout/activity_main.xml index 51bc9ab..0bc1317 100644 --- a/example/app/src/main/res/layout/activity_main.xml +++ b/example/app/src/main/res/layout/activity_main.xml @@ -206,7 +206,7 @@ android:textColor="@color/colorTextSecondary" /> - + + android:layout_marginEnd="4dp" /> + + android:layout_marginStart="4dp" /> diff --git a/example/app/src/main/res/layout/item_direct_answer_history.xml b/example/app/src/main/res/layout/item_direct_answer_history.xml new file mode 100644 index 0000000..db24aeb --- /dev/null +++ b/example/app/src/main/res/layout/item_direct_answer_history.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + diff --git a/example/app/src/main/res/values/strings.xml b/example/app/src/main/res/values/strings.xml index cce868f..bfb0d78 100644 --- a/example/app/src/main/res/values/strings.xml +++ b/example/app/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ 平衡 创意 生成 + 直答 优化提示词 生成结果将显示在这里… 复制 @@ -24,9 +25,11 @@ 历史 生成历史 返回 - 优化历史 - 生成历史 + 优化 + 生成 + 直答 暂无优化记录 暂无生成记录 + 暂无直答记录 使用