feat: 恢复模型直答功能及直答历史
Some checks failed
Flask 提示词大师 - CI/CD 流水线 / 代码质量检查 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 单元测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 集成测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 构建Docker镜像 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到测试环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到生产环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署监控系统 (push) Has been cancelled

This commit is contained in:
2026-03-04 22:33:57 +08:00
parent b4fca4a338
commit 4ed7b82139
13 changed files with 358 additions and 16 deletions

View File

@@ -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);
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();
} else {
binding.recyclerOptimize.setVisibility(View.GONE);
binding.recyclerGeneration.setVisibility(View.VISIBLE);
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);
}
}

View File

@@ -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 -> {

View File

@@ -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<DirectAnswerHistoryAdapter.ViewHolder> {
private final List<DirectAnswerRecord> 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<DirectAnswerRecord> 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);
}
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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<List<DirectAnswerRecord>> 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();
}

View File

@@ -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<java.util.List<OptimizeRecord>> getRecentOptimizeRecords() {
return optimizeDao.getRecentRecords();
}
public LiveData<java.util.List<DirectAnswerRecord>> getRecentDirectAnswerRecords() {
return directAnswerDao.getRecentRecords();
}
/**
* 智能提示词优化(两阶段专家逻辑)。需在后台线程调用。
*/

View File

@@ -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<List<GenerationRecord>> getGenerationRecords() {
return repository.getRecentRecords();
}
public LiveData<List<DirectAnswerRecord>> getDirectAnswerRecords() {
return repository.getRecentDirectAnswerRecords();
}
}

View File

@@ -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()) {

View File

@@ -67,6 +67,15 @@
android:clipToPadding="false"
tools:listitem="@layout/item_generation_history" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_direct_answer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:padding="12dp"
android:clipToPadding="false"
tools:listitem="@layout/item_direct_answer_history" />
<TextView
android:id="@+id/empty_optimize"
android:layout_width="wrap_content"
@@ -84,5 +93,14 @@
android:text="@string/history_empty_generation"
android:textColor="@color/colorTextSecondary"
android:visibility="gone" />
<TextView
android:id="@+id/empty_direct_answer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/history_empty_direct_answer"
android:textColor="@color/colorTextSecondary"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

View File

@@ -206,7 +206,7 @@
android:textColor="@color/colorTextSecondary" />
</LinearLayout>
<!-- 操作按钮区:优化 + 生成 -->
<!-- 操作按钮区:优化 + 生成 + 直答 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -220,16 +220,25 @@
android:layout_height="56dp"
android:layout_weight="1"
android:text="@string/btn_optimize"
android:layout_marginEnd="8dp" />
android:layout_marginEnd="4dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_generate"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:text="@string/btn_generate"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_direct_answer"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:text="@string/btn_direct_answer"
app:backgroundTint="@color/colorPrimary"
android:textColor="@color/colorWhite"
android:layout_marginStart="8dp" />
android:layout_marginStart="4dp" />
</LinearLayout>
<!-- 加载指示 -->

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/colorCard"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
android:layout_marginBottom="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_question"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/colorTextDark"
android:textSize="14sp"
android:maxLines="2"
android:ellipsize="end"
tools:text="问:什么是机器学习?" />
<TextView
android:id="@+id/tv_answer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/colorText"
android:textSize="13sp"
android:maxLines="4"
android:ellipsize="end"
tools:text="答:机器学习是..." />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/colorTextSecondary"
android:textSize="11sp"
tools:text="2025-03-02 14:30" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_use"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/history_use" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_copy"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_copy" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -16,6 +16,7 @@
<string name="temp_balanced">平衡</string>
<string name="temp_creative">创意</string>
<string name="btn_generate">生成</string>
<string name="btn_direct_answer">直答</string>
<string name="btn_optimize">优化提示词</string>
<string name="result_placeholder">生成结果将显示在这里…</string>
<string name="btn_copy">复制</string>
@@ -24,9 +25,11 @@
<string name="btn_history">历史</string>
<string name="history_title">生成历史</string>
<string name="history_back">返回</string>
<string name="history_tab_optimize">优化历史</string>
<string name="history_tab_generation">生成历史</string>
<string name="history_tab_optimize">优化</string>
<string name="history_tab_generation">生成</string>
<string name="history_tab_direct_answer">直答</string>
<string name="history_empty_optimize">暂无优化记录</string>
<string name="history_empty_generation">暂无生成记录</string>
<string name="history_empty_direct_answer">暂无直答记录</string>
<string name="history_use">使用</string>
</resources>