diff --git a/example/app/src/main/AndroidManifest.xml b/example/app/src/main/AndroidManifest.xml
index d9d1e0c..70edeba 100644
--- a/example/app/src/main/AndroidManifest.xml
+++ b/example/app/src/main/AndroidManifest.xml
@@ -34,6 +34,26 @@
android:name=".MealPlanningHistoryActivity"
android:exported="false"
android:screenOrientation="portrait" />
+
+
+
+
+
activityClass;
+ final boolean placeholder;
+
+ Entry(String title, String subtitle, String badge, @Nullable Class> activityClass) {
+ this.title = title;
+ this.subtitle = subtitle;
+ this.badge = badge;
+ this.activityClass = activityClass;
+ this.placeholder = (activityClass == null);
+ }
+ }
+
+ private final List entries = new ArrayList<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActivityAiServicesBinding binding = ActivityAiServicesBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ buildEntries();
+ binding.btnBack.setOnClickListener(v -> finish());
+
+ LinearLayout container = binding.containerAiList;
+ LayoutInflater inflater = getLayoutInflater();
+ for (int i = 0; i < entries.size(); i++) {
+ Entry e = entries.get(i);
+ ItemAiServiceBinding itemBinding = ItemAiServiceBinding.inflate(inflater, container, false);
+ itemBinding.itemTitle.setText(e.title);
+ itemBinding.itemSubtitle.setText(e.subtitle);
+ itemBinding.itemBadge.setText(e.badge);
+ itemBinding.itemBadge.setVisibility(View.VISIBLE);
+ final int index = i;
+ itemBinding.getRoot().setOnClickListener(v -> onItemClick(entries.get(index)));
+ container.addView(itemBinding.getRoot());
+ }
+ }
+
+ private void buildEntries() {
+ entries.clear();
+ // 有参考实现的:跳转对应 Activity
+ entries.add(new Entry("智能饭菜规划", "AI智能推荐营养搭配,让每一餐都健康美味", "热门", MealPlanningActivity.class));
+ entries.add(new Entry("提示词优化", "两阶段专家生成专业提示词", "通用", null)); // 主界面已有,可跳回主界面
+ entries.add(new Entry("旅行攻略规划", "根据目的地与天数,AI 生成行程与实用建议", "出行", TravelPlanningActivity.class));
+ entries.add(new Entry("智能周报生成", "输入工作要点,一键生成规范周报/日报", "办公", WeeklyReportActivity.class));
+ entries.add(new Entry("会议纪要整理", "粘贴会议转写或要点,一键生成结构化会议纪要", "办公", MeetingMinutesActivity.class));
+ // 以下为占位
+ entries.add(new Entry("古诗词解析", "深度解析古诗词意境,感受中华文化之美", "文化", null));
+ entries.add(new Entry("古诗词收藏", "收藏喜爱的古诗词,建立个人文化宝库", "收藏", null));
+ entries.add(new Entry("简历/求职信优化", "润色简历或根据岗位生成针对性求职信", "求职", null));
+ entries.add(new Entry("智能提示词优化2号专家", "需求分析 + 领域专家生成高质量提示词", "专家", null));
+ entries.add(new Entry("智能提示词优化3号专家", "需求分析 + 领域专家生成,含历史记录", "专家", null));
+ entries.add(new Entry("读书笔记/摘要", "生成摘要、金句、思维导图式大纲或读后感", "文化", null));
+ entries.add(new Entry("育儿/教育助手", "育儿建议、睡前故事、简单科普或习题讲解", "教育", null));
+ entries.add(new Entry("健身/运动计划", "根据目标与场地生成一周训练计划", "健康", null));
+ entries.add(new Entry("合同/条款解读", "人话版要点、风险提示与建议关注条款", "法律", null));
+ entries.add(new Entry("小红书/短视频脚本", "生成标题、分镜文案、口播稿", "创作", null));
+ entries.add(new Entry("面试模拟/问答准备", "按岗位类型出题并给参考答案与点评", "求职", null));
+ entries.add(new Entry("节日/祝福语生成", "祝福语、贺卡文案、红包配文", "生活", null));
+ entries.add(new Entry("个人复盘/周复盘", "结构化复盘与下周重点", "成长", null));
+ entries.add(new Entry("待办/日程整理", "零散待办生成按优先级与日期的日程清单", "效率", null));
+ entries.add(new Entry("邮件润色/回复建议", "草稿或要点生成得体邮件回复", "办公", null));
+ entries.add(new Entry("演讲稿/汇报稿", "根据主题与场合生成演讲稿或汇报大纲", "办公", null));
+ entries.add(new Entry("取名/品牌名", "公司名、产品名或宝宝取名建议", "创意", null));
+ entries.add(new Entry("菜谱/做法", "根据食材或菜名生成详细做法", "生活", null));
+ entries.add(new Entry("学习计划/备考规划", "生成学习计划与复习建议", "教育", null));
+ }
+
+ private void onItemClick(Entry e) {
+ if (e.activityClass != null) {
+ startActivity(new Intent(this, e.activityClass));
+ } else if ("提示词优化".equals(e.title)) {
+ // 主界面已有优化提示词,返回主界面
+ startActivity(new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
+ finish();
+ } else {
+ Intent intent = new Intent(this, PlaceholderActivity.class);
+ intent.putExtra(PlaceholderActivity.EXTRA_TITLE, e.title);
+ intent.putExtra(PlaceholderActivity.EXTRA_SUBTITLE, e.subtitle);
+ startActivity(intent);
+ }
+ }
+}
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 a5fb71c..5e287ac 100644
--- a/example/app/src/main/java/com/ruilaizi/example/MainActivity.java
+++ b/example/app/src/main/java/com/ruilaizi/example/MainActivity.java
@@ -48,8 +48,8 @@ public class MainActivity extends AppCompatActivity {
if (binding.btnHistory != null) {
binding.btnHistory.setOnClickListener(v -> openHistory());
}
- if (binding.btnMealPlanning != null) {
- binding.btnMealPlanning.setOnClickListener(v -> startActivity(new Intent(this, MealPlanningActivity.class)));
+ if (binding.btnAiServices != null) {
+ binding.btnAiServices.setOnClickListener(v -> startActivity(new Intent(this, AIServicesActivity.class)));
}
observeViewModel();
showApiKeyHintIfNeeded();
diff --git a/example/app/src/main/java/com/ruilaizi/example/MeetingMinutesActivity.java b/example/app/src/main/java/com/ruilaizi/example/MeetingMinutesActivity.java
new file mode 100644
index 0000000..226c6a5
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/MeetingMinutesActivity.java
@@ -0,0 +1,87 @@
+package com.ruilaizi.example;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.textfield.TextInputEditText;
+import com.ruilaizi.example.databinding.ActivityMeetingMinutesBinding;
+import com.ruilaizi.example.ui.MeetingMinutesViewModel;
+
+public class MeetingMinutesActivity extends AppCompatActivity {
+
+ private ActivityMeetingMinutesBinding binding;
+ private MeetingMinutesViewModel viewModel;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityMeetingMinutesBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ viewModel = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(MeetingMinutesViewModel.class);
+
+ binding.btnBack.setOnClickListener(v -> finish());
+ binding.btnGenerate.setOnClickListener(v -> doGenerate());
+ binding.btnCopy.setOnClickListener(v -> copyResult());
+ observeViewModel();
+ }
+
+ private void doGenerate() {
+ String raw = getText(binding.etRawContent);
+ if (raw.isEmpty()) {
+ Toast.makeText(this, "请粘贴或输入会议内容", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ String title = getText(binding.etTitle);
+ viewModel.generate(raw, title);
+ }
+
+ private String getText(TextInputEditText et) {
+ return et.getText() != null ? et.getText().toString().trim() : "";
+ }
+
+ private void copyResult() {
+ String text = binding.tvResult.getText() != null ? binding.tvResult.getText().toString() : "";
+ if (text.isEmpty()) {
+ Snackbar.make(binding.getRoot(), "暂无内容可复制", Snackbar.LENGTH_SHORT).show();
+ return;
+ }
+ ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm != null) {
+ cm.setPrimaryClip(ClipData.newPlainText("会议纪要", text));
+ Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show();
+ }
+ }
+
+ private void observeViewModel() {
+ viewModel.getResultLiveData().observe(this, text -> {
+ if (text != null) {
+ binding.tvResult.setText(text);
+ binding.tvResult.setTextColor(getResources().getColor(R.color.colorTextDark, getTheme()));
+ }
+ });
+ viewModel.getLoadingLiveData().observe(this, loading -> {
+ binding.progressBar.setVisibility(loading != null && loading ? View.VISIBLE : View.GONE);
+ binding.btnGenerate.setEnabled(loading == null || !loading);
+ });
+ viewModel.getErrorLiveData().observe(this, error -> {
+ if (error != null && !error.isEmpty()) {
+ new AlertDialog.Builder(this).setMessage(error).setPositiveButton(android.R.string.ok, (d, w) -> viewModel.clearError()).show();
+ }
+ });
+ viewModel.getSnackbarLiveData().observe(this, msg -> {
+ if (msg != null && !msg.isEmpty()) {
+ Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
+ viewModel.clearSnackbar();
+ }
+ });
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/PlaceholderActivity.java b/example/app/src/main/java/com/ruilaizi/example/PlaceholderActivity.java
new file mode 100644
index 0000000..42b49a5
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/PlaceholderActivity.java
@@ -0,0 +1,34 @@
+package com.ruilaizi.example;
+
+import android.os.Bundle;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.ruilaizi.example.databinding.ActivityPlaceholderBinding;
+
+/**
+ * 占位页:无参考实现的功能仅显示标题与「敬请期待」。
+ */
+public class PlaceholderActivity extends AppCompatActivity {
+
+ public static final String EXTRA_TITLE = "title";
+ public static final String EXTRA_SUBTITLE = "subtitle";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ActivityPlaceholderBinding binding = ActivityPlaceholderBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ String title = getIntent() != null && getIntent().hasExtra(EXTRA_TITLE)
+ ? getIntent().getStringExtra(EXTRA_TITLE) : "AI 应用";
+ String subtitle = getIntent() != null && getIntent().hasExtra(EXTRA_SUBTITLE)
+ ? getIntent().getStringExtra(EXTRA_SUBTITLE) : "";
+
+ binding.placeholderTitle.setText(title);
+ binding.placeholderSubtitle.setText(subtitle != null ? subtitle : "");
+ binding.placeholderHint.setText(R.string.placeholder_coming_soon);
+ binding.btnBack.setOnClickListener(v -> finish());
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/TravelPlanningActivity.java b/example/app/src/main/java/com/ruilaizi/example/TravelPlanningActivity.java
new file mode 100644
index 0000000..2049e2f
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/TravelPlanningActivity.java
@@ -0,0 +1,84 @@
+package com.ruilaizi.example;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProvider;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.textfield.TextInputEditText;
+import com.ruilaizi.example.databinding.ActivityTravelPlanningBinding;
+import com.ruilaizi.example.ui.TravelPlanningViewModel;
+
+public class TravelPlanningActivity extends AppCompatActivity {
+ private ActivityTravelPlanningBinding binding;
+ private TravelPlanningViewModel viewModel;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityTravelPlanningBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ viewModel = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(TravelPlanningViewModel.class);
+ binding.btnBack.setOnClickListener(v -> finish());
+ binding.btnGenerate.setOnClickListener(v -> doGenerate());
+ binding.btnCopy.setOnClickListener(v -> copyResult());
+ observeViewModel();
+ }
+
+ private void doGenerate() {
+ String dest = getText(binding.etDestination);
+ if (dest.isEmpty()) {
+ android.widget.Toast.makeText(this, "请填写目的地", android.widget.Toast.LENGTH_SHORT).show();
+ return;
+ }
+ String days = getText(binding.etDays);
+ String people = getText(binding.etPeople);
+ String prefs = getText(binding.etPreferences);
+ viewModel.generate(dest, days.isEmpty() ? "3" : days, people.isEmpty() ? "2" : people, prefs, "适中");
+ }
+
+ private String getText(TextInputEditText et) {
+ return et.getText() != null ? et.getText().toString().trim() : "";
+ }
+
+ private void copyResult() {
+ String text = binding.tvResult.getText() != null ? binding.tvResult.getText().toString() : "";
+ if (text.isEmpty()) {
+ Snackbar.make(binding.getRoot(), "暂无内容可复制", Snackbar.LENGTH_SHORT).show();
+ return;
+ }
+ ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm != null) {
+ cm.setPrimaryClip(ClipData.newPlainText("旅行攻略", text));
+ Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show();
+ }
+ }
+
+ private void observeViewModel() {
+ viewModel.getResultLiveData().observe(this, text -> {
+ if (text != null) {
+ binding.tvResult.setText(text);
+ binding.tvResult.setTextColor(getResources().getColor(R.color.colorTextDark, getTheme()));
+ }
+ });
+ viewModel.getLoadingLiveData().observe(this, loading -> {
+ binding.progressBar.setVisibility(loading != null && loading ? View.VISIBLE : View.GONE);
+ binding.btnGenerate.setEnabled(loading == null || !loading);
+ });
+ viewModel.getErrorLiveData().observe(this, error -> {
+ if (error != null && !error.isEmpty()) {
+ new AlertDialog.Builder(this).setMessage(error).setPositiveButton(android.R.string.ok, (d, w) -> viewModel.clearError()).show();
+ }
+ });
+ viewModel.getSnackbarLiveData().observe(this, msg -> {
+ if (msg != null && !msg.isEmpty()) {
+ Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
+ viewModel.clearSnackbar();
+ }
+ });
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/WeeklyReportActivity.java b/example/app/src/main/java/com/ruilaizi/example/WeeklyReportActivity.java
new file mode 100644
index 0000000..83070b9
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/WeeklyReportActivity.java
@@ -0,0 +1,86 @@
+package com.ruilaizi.example;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.textfield.TextInputEditText;
+import com.ruilaizi.example.databinding.ActivityWeeklyReportBinding;
+import com.ruilaizi.example.ui.WeeklyReportViewModel;
+
+public class WeeklyReportActivity extends AppCompatActivity {
+
+ private ActivityWeeklyReportBinding binding;
+ private WeeklyReportViewModel viewModel;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityWeeklyReportBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ viewModel = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(WeeklyReportViewModel.class);
+
+ binding.btnBack.setOnClickListener(v -> finish());
+ binding.btnGenerate.setOnClickListener(v -> doGenerate());
+ binding.btnCopy.setOnClickListener(v -> copyResult());
+ observeViewModel();
+ }
+
+ private void doGenerate() {
+ String content = getText(binding.etContent);
+ if (content.isEmpty()) {
+ Toast.makeText(this, "请填写工作要点或内容", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ viewModel.generate("weekly", content);
+ }
+
+ private String getText(TextInputEditText et) {
+ return et.getText() != null ? et.getText().toString().trim() : "";
+ }
+
+ private void copyResult() {
+ String text = binding.tvResult.getText() != null ? binding.tvResult.getText().toString() : "";
+ if (text.isEmpty()) {
+ Snackbar.make(binding.getRoot(), "暂无内容可复制", Snackbar.LENGTH_SHORT).show();
+ return;
+ }
+ ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm != null) {
+ cm.setPrimaryClip(ClipData.newPlainText("周报", text));
+ Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show();
+ }
+ }
+
+ private void observeViewModel() {
+ viewModel.getResultLiveData().observe(this, text -> {
+ if (text != null) {
+ binding.tvResult.setText(text);
+ binding.tvResult.setTextColor(getResources().getColor(R.color.colorTextDark, getTheme()));
+ }
+ });
+ viewModel.getLoadingLiveData().observe(this, loading -> {
+ binding.progressBar.setVisibility(loading != null && loading ? View.VISIBLE : View.GONE);
+ binding.btnGenerate.setEnabled(loading == null || !loading);
+ });
+ viewModel.getErrorLiveData().observe(this, error -> {
+ if (error != null && !error.isEmpty()) {
+ new AlertDialog.Builder(this).setMessage(error).setPositiveButton(android.R.string.ok, (d, w) -> viewModel.clearError()).show();
+ }
+ });
+ viewModel.getSnackbarLiveData().observe(this, msg -> {
+ if (msg != null && !msg.isEmpty()) {
+ Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
+ viewModel.clearSnackbar();
+ }
+ });
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/meeting/MeetingMinutesGenerator.java b/example/app/src/main/java/com/ruilaizi/example/data/meeting/MeetingMinutesGenerator.java
new file mode 100644
index 0000000..d7923c2
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/meeting/MeetingMinutesGenerator.java
@@ -0,0 +1,31 @@
+package com.ruilaizi.example.data.meeting;
+
+import com.ruilaizi.example.data.api.ApiKeyProvider;
+import com.ruilaizi.example.data.api.OpenAICompletionService;
+
+/**
+ * 会议纪要整理生成器,与 Flask meeting_minutes 逻辑一致。
+ */
+public class MeetingMinutesGenerator {
+
+ private static final String SYSTEM_PROMPT = "你是一位专业的会议记录助手,擅长将零散的会议转写或要点整理成结构清晰的会议纪要。\n\n要求:\n"
+ + "1. 从用户输入中提炼:会议主题、时间/参与人(如有)、讨论要点、结论与决议、待办事项(含责任人/截止时间若可推断)。\n"
+ + "2. 使用 Markdown 格式:一级标题为「一、会议概要」「二、讨论要点」「三、结论与决议」「四、待办事项」等,二级标题和列表项简明扼要。\n"
+ + "3. 语气正式、条理清楚,便于归档和分享。若信息不足可合理归纳,不要编造具体人名与数据。";
+
+ private final OpenAICompletionService completionService;
+ private final ApiKeyProvider apiKeyProvider;
+
+ public MeetingMinutesGenerator(OpenAICompletionService completionService, ApiKeyProvider apiKeyProvider) {
+ this.completionService = completionService;
+ this.apiKeyProvider = apiKeyProvider;
+ }
+
+ public String generate(String rawContent, String title) throws Exception {
+ String apiKey = apiKeyProvider.getApiKey();
+ if (apiKey == null || apiKey.isEmpty()) throw new Exception("请先在设置中配置 API Key");
+ if (rawContent == null || rawContent.trim().isEmpty()) throw new Exception("请粘贴或输入会议内容");
+ String userPrompt = "请将以下会议内容整理成会议纪要:\n\n" + (title != null && !title.isEmpty() ? "会议主题:" + title + "\n\n" : "") + rawContent.trim();
+ return completionService.chat(SYSTEM_PROMPT, userPrompt, 0.4f, 2000, apiKey);
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/meeting/MeetingMinutesRepository.java b/example/app/src/main/java/com/ruilaizi/example/data/meeting/MeetingMinutesRepository.java
new file mode 100644
index 0000000..2e2fb80
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/meeting/MeetingMinutesRepository.java
@@ -0,0 +1,20 @@
+package com.ruilaizi.example.data.meeting;
+
+import android.content.Context;
+
+import com.ruilaizi.example.data.api.ApiKeyProvider;
+import com.ruilaizi.example.data.api.OpenAICompletionService;
+
+public class MeetingMinutesRepository {
+
+ private final MeetingMinutesGenerator generator;
+
+ public MeetingMinutesRepository(Context context) {
+ ApiKeyProvider apiKeyProvider = new ApiKeyProvider(context);
+ generator = new MeetingMinutesGenerator(new OpenAICompletionService(null), apiKeyProvider);
+ }
+
+ public String generate(String rawContent, String title) throws Exception {
+ return generator.generate(rawContent, title);
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/travel/TravelPlanGenerator.java b/example/app/src/main/java/com/ruilaizi/example/data/travel/TravelPlanGenerator.java
new file mode 100644
index 0000000..b05e2f2
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/travel/TravelPlanGenerator.java
@@ -0,0 +1,30 @@
+package com.ruilaizi.example.data.travel;
+
+import com.ruilaizi.example.data.api.ApiKeyProvider;
+import com.ruilaizi.example.data.api.OpenAICompletionService;
+
+/** 旅行攻略生成器,与 Flask travel_planning 逻辑一致。 */
+public class TravelPlanGenerator {
+
+ private static final String SYSTEM_PROMPT = "你是一位专业的旅行规划师,擅长根据目的地、天数和偏好生成实用、可执行的旅行攻略。\n\n"
+ + "请按以下 **Markdown 结构** 输出:\n**一、行程概览**\n**二、每日行程**(按天,上午/下午/晚上)\n"
+ + "**三、实用信息**(交通、住宿、美食、预算、注意事项)。要求:具体可执行、符合预算与偏好。";
+
+ private final OpenAICompletionService completionService;
+ private final ApiKeyProvider apiKeyProvider;
+
+ public TravelPlanGenerator(OpenAICompletionService completionService, ApiKeyProvider apiKeyProvider) {
+ this.completionService = completionService;
+ this.apiKeyProvider = apiKeyProvider;
+ }
+
+ public String generate(String destination, String days, String people, String preferences, String budget) throws Exception {
+ String apiKey = apiKeyProvider.getApiKey();
+ if (apiKey == null || apiKey.isEmpty()) throw new Exception("请先在设置中配置 API Key");
+ if (destination == null || destination.trim().isEmpty()) throw new Exception("请填写目的地");
+ String user = "目的地:" + destination.trim() + "。出行天数:" + (days != null ? days : "3")
+ + "。人数:" + (people != null ? people : "2") + "。偏好:" + (preferences != null ? preferences : "无")
+ + "。预算:" + (budget != null ? budget : "适中") + "。请生成旅行攻略。";
+ return completionService.chat(SYSTEM_PROMPT, user, 0.6f, 2000, apiKey);
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/travel/TravelPlanningRepository.java b/example/app/src/main/java/com/ruilaizi/example/data/travel/TravelPlanningRepository.java
new file mode 100644
index 0000000..1abe278
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/travel/TravelPlanningRepository.java
@@ -0,0 +1,20 @@
+package com.ruilaizi.example.data.travel;
+
+import android.content.Context;
+
+import com.ruilaizi.example.data.api.ApiKeyProvider;
+import com.ruilaizi.example.data.api.OpenAICompletionService;
+
+public class TravelPlanningRepository {
+
+ private final TravelPlanGenerator generator;
+
+ public TravelPlanningRepository(Context context) {
+ ApiKeyProvider apiKeyProvider = new ApiKeyProvider(context);
+ generator = new TravelPlanGenerator(new OpenAICompletionService(null), apiKeyProvider);
+ }
+
+ public String generate(String destination, String days, String people, String preferences, String budget) throws Exception {
+ return generator.generate(destination, days, people, preferences, budget);
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/weekly/WeeklyReportGenerator.java b/example/app/src/main/java/com/ruilaizi/example/data/weekly/WeeklyReportGenerator.java
new file mode 100644
index 0000000..2948383
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/weekly/WeeklyReportGenerator.java
@@ -0,0 +1,32 @@
+package com.ruilaizi.example.data.weekly;
+
+import com.ruilaizi.example.data.api.ApiKeyProvider;
+import com.ruilaizi.example.data.api.OpenAICompletionService;
+
+/**
+ * 智能周报/日报生成器,与 Flask weekly_report 逻辑一致。
+ */
+public class WeeklyReportGenerator {
+
+ private final OpenAICompletionService completionService;
+ private final ApiKeyProvider apiKeyProvider;
+
+ public WeeklyReportGenerator(OpenAICompletionService completionService, ApiKeyProvider apiKeyProvider) {
+ this.completionService = completionService;
+ this.apiKeyProvider = apiKeyProvider;
+ }
+
+ public String generate(String reportType, String content) throws Exception {
+ String apiKey = apiKeyProvider.getApiKey();
+ if (apiKey == null || apiKey.isEmpty()) throw new Exception("请先在设置中配置 API Key");
+ if (content == null || content.trim().isEmpty()) throw new Exception("请填写工作要点或内容");
+ String typeName = "weekly".equals(reportType) ? "周报" : "日报";
+ String systemPrompt = "你是一位专业的职场写作助手,擅长将零散的工作要点整理成规范、简洁的" + typeName + "。\n\n要求:\n"
+ + "1. 根据用户输入的工作要点或流水记录,归纳成结构清晰的" + typeName + "。\n"
+ + "2. 使用 Markdown 格式:一级标题为「一、本周/今日工作完成情况」等,二级标题区分模块,列表项简明扼要。\n"
+ + "3. 可适当补充「二、下周/明日计划」「三、问题与风险」等小节(若用户未提供则简要生成)。\n"
+ + "4. 语气正式、条理清楚,便于直接复制到飞书/钉钉/邮件。";
+ String userPrompt = "请将以下内容整理成一份" + typeName + ":\n\n" + content.trim();
+ return completionService.chat(systemPrompt, userPrompt, 0.5f, 1500, apiKey);
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/data/weekly/WeeklyReportRepository.java b/example/app/src/main/java/com/ruilaizi/example/data/weekly/WeeklyReportRepository.java
new file mode 100644
index 0000000..854b4b9
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/data/weekly/WeeklyReportRepository.java
@@ -0,0 +1,20 @@
+package com.ruilaizi.example.data.weekly;
+
+import android.content.Context;
+
+import com.ruilaizi.example.data.api.ApiKeyProvider;
+import com.ruilaizi.example.data.api.OpenAICompletionService;
+
+public class WeeklyReportRepository {
+
+ private final WeeklyReportGenerator generator;
+
+ public WeeklyReportRepository(Context context) {
+ ApiKeyProvider apiKeyProvider = new ApiKeyProvider(context);
+ generator = new WeeklyReportGenerator(new OpenAICompletionService(null), apiKeyProvider);
+ }
+
+ public String generate(String reportType, String content) throws Exception {
+ return generator.generate(reportType, content);
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/ui/MeetingMinutesViewModel.java b/example/app/src/main/java/com/ruilaizi/example/ui/MeetingMinutesViewModel.java
new file mode 100644
index 0000000..293bfb0
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/ui/MeetingMinutesViewModel.java
@@ -0,0 +1,59 @@
+package com.ruilaizi.example.ui;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.ruilaizi.example.data.meeting.MeetingMinutesRepository;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class MeetingMinutesViewModel extends AndroidViewModel {
+
+ private final MeetingMinutesRepository repository;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ private final MutableLiveData resultLiveData = new MutableLiveData<>("");
+ private final MutableLiveData loadingLiveData = new MutableLiveData<>(false);
+ private final MutableLiveData errorLiveData = new MutableLiveData<>();
+ private final MutableLiveData snackbarLiveData = new MutableLiveData<>();
+
+ public MeetingMinutesViewModel(@NonNull Application app) {
+ super(app);
+ repository = new MeetingMinutesRepository(app);
+ }
+
+ public LiveData getResultLiveData() { return resultLiveData; }
+ public LiveData getLoadingLiveData() { return loadingLiveData; }
+ public LiveData getErrorLiveData() { return errorLiveData; }
+ public LiveData getSnackbarLiveData() { return snackbarLiveData; }
+
+ public void generate(String rawContent, String title) {
+ loadingLiveData.setValue(true);
+ resultLiveData.setValue("");
+ errorLiveData.setValue(null);
+ executor.execute(() -> {
+ try {
+ String summary = repository.generate(rawContent, title);
+ resultLiveData.postValue(summary);
+ snackbarLiveData.postValue("会议纪要生成完成");
+ } catch (Exception e) {
+ errorLiveData.postValue(e != null ? e.getMessage() : "生成失败");
+ } finally {
+ loadingLiveData.postValue(false);
+ }
+ });
+ }
+
+ public void clearError() { errorLiveData.setValue(null); }
+ public void clearSnackbar() { snackbarLiveData.setValue(null); }
+
+ @Override
+ protected void onCleared() {
+ executor.shutdown();
+ super.onCleared();
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/ui/TravelPlanningViewModel.java b/example/app/src/main/java/com/ruilaizi/example/ui/TravelPlanningViewModel.java
new file mode 100644
index 0000000..f6e8288
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/ui/TravelPlanningViewModel.java
@@ -0,0 +1,59 @@
+package com.ruilaizi.example.ui;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.ruilaizi.example.data.travel.TravelPlanningRepository;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class TravelPlanningViewModel extends AndroidViewModel {
+
+ private final TravelPlanningRepository repository;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ private final MutableLiveData resultLiveData = new MutableLiveData<>("");
+ private final MutableLiveData loadingLiveData = new MutableLiveData<>(false);
+ private final MutableLiveData errorLiveData = new MutableLiveData<>();
+ private final MutableLiveData snackbarLiveData = new MutableLiveData<>();
+
+ public TravelPlanningViewModel(@NonNull Application app) {
+ super(app);
+ repository = new TravelPlanningRepository(app);
+ }
+
+ public LiveData getResultLiveData() { return resultLiveData; }
+ public LiveData getLoadingLiveData() { return loadingLiveData; }
+ public LiveData getErrorLiveData() { return errorLiveData; }
+ public LiveData getSnackbarLiveData() { return snackbarLiveData; }
+
+ public void generate(String destination, String days, String people, String preferences, String budget) {
+ loadingLiveData.setValue(true);
+ resultLiveData.setValue("");
+ errorLiveData.setValue(null);
+ executor.execute(() -> {
+ try {
+ String plan = repository.generate(destination, days, people, preferences, budget);
+ resultLiveData.postValue(plan);
+ snackbarLiveData.postValue("旅行攻略生成完成");
+ } catch (Exception e) {
+ errorLiveData.postValue(e != null ? e.getMessage() : "生成失败");
+ } finally {
+ loadingLiveData.postValue(false);
+ }
+ });
+ }
+
+ public void clearError() { errorLiveData.setValue(null); }
+ public void clearSnackbar() { snackbarLiveData.setValue(null); }
+
+ @Override
+ protected void onCleared() {
+ executor.shutdown();
+ super.onCleared();
+ }
+}
diff --git a/example/app/src/main/java/com/ruilaizi/example/ui/WeeklyReportViewModel.java b/example/app/src/main/java/com/ruilaizi/example/ui/WeeklyReportViewModel.java
new file mode 100644
index 0000000..89102f4
--- /dev/null
+++ b/example/app/src/main/java/com/ruilaizi/example/ui/WeeklyReportViewModel.java
@@ -0,0 +1,59 @@
+package com.ruilaizi.example.ui;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.ruilaizi.example.data.weekly.WeeklyReportRepository;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class WeeklyReportViewModel extends AndroidViewModel {
+
+ private final WeeklyReportRepository repository;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ private final MutableLiveData resultLiveData = new MutableLiveData<>("");
+ private final MutableLiveData loadingLiveData = new MutableLiveData<>(false);
+ private final MutableLiveData errorLiveData = new MutableLiveData<>();
+ private final MutableLiveData snackbarLiveData = new MutableLiveData<>();
+
+ public WeeklyReportViewModel(@NonNull Application app) {
+ super(app);
+ repository = new WeeklyReportRepository(app);
+ }
+
+ public LiveData getResultLiveData() { return resultLiveData; }
+ public LiveData getLoadingLiveData() { return loadingLiveData; }
+ public LiveData getErrorLiveData() { return errorLiveData; }
+ public LiveData getSnackbarLiveData() { return snackbarLiveData; }
+
+ public void generate(String reportType, String content) {
+ loadingLiveData.setValue(true);
+ resultLiveData.setValue("");
+ errorLiveData.setValue(null);
+ executor.execute(() -> {
+ try {
+ String report = repository.generate(reportType, content);
+ resultLiveData.postValue(report);
+ snackbarLiveData.postValue("周报/日报生成完成");
+ } catch (Exception e) {
+ errorLiveData.postValue(e != null ? e.getMessage() : "生成失败");
+ } finally {
+ loadingLiveData.postValue(false);
+ }
+ });
+ }
+
+ public void clearError() { errorLiveData.setValue(null); }
+ public void clearSnackbar() { snackbarLiveData.setValue(null); }
+
+ @Override
+ protected void onCleared() {
+ executor.shutdown();
+ super.onCleared();
+ }
+}
diff --git a/example/app/src/main/res/layout/activity_ai_services.xml b/example/app/src/main/res/layout/activity_ai_services.xml
new file mode 100644
index 0000000..df59d76
--- /dev/null
+++ b/example/app/src/main/res/layout/activity_ai_services.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/layout/activity_main.xml b/example/app/src/main/res/layout/activity_main.xml
index badeec5..1ecef64 100644
--- a/example/app/src/main/res/layout/activity_main.xml
+++ b/example/app/src/main/res/layout/activity_main.xml
@@ -29,11 +29,11 @@
android:textSize="24sp"
android:textStyle="bold" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/layout/activity_placeholder.xml b/example/app/src/main/res/layout/activity_placeholder.xml
new file mode 100644
index 0000000..58e0b56
--- /dev/null
+++ b/example/app/src/main/res/layout/activity_placeholder.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/layout/activity_travel_planning.xml b/example/app/src/main/res/layout/activity_travel_planning.xml
new file mode 100644
index 0000000..bc06cee
--- /dev/null
+++ b/example/app/src/main/res/layout/activity_travel_planning.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/layout/activity_weekly_report.xml b/example/app/src/main/res/layout/activity_weekly_report.xml
new file mode 100644
index 0000000..832b248
--- /dev/null
+++ b/example/app/src/main/res/layout/activity_weekly_report.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/layout/item_ai_service.xml b/example/app/src/main/res/layout/item_ai_service.xml
new file mode 100644
index 0000000..9c5ef0e
--- /dev/null
+++ b/example/app/src/main/res/layout/item_ai_service.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/app/src/main/res/values/strings.xml b/example/app/src/main/res/values/strings.xml
index 5c34973..f659fa0 100644
--- a/example/app/src/main/res/values/strings.xml
+++ b/example/app/src/main/res/values/strings.xml
@@ -36,6 +36,10 @@
生成饭菜规划
填写左侧参数后点击「生成饭菜规划」
饭菜规划
+ AI应用
规划历史
暂无规划记录
+ 敬请期待
+ AI 智能应用
+ 探索更多 AI 应用