From 5efd79ddfc56a6dc80245122cd96b14f85f6d977 Mon Sep 17 00:00:00 2001 From: rjb <263303411@qq.com> Date: Wed, 4 Mar 2026 23:24:28 +0800 Subject: [PATCH] =?UTF-8?q?ai=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/app/src/main/AndroidManifest.xml | 20 +++ .../ruilaizi/example/AIServicesActivity.java | 111 +++++++++++++++++ .../com/ruilaizi/example/MainActivity.java | 4 +- .../example/MeetingMinutesActivity.java | 87 +++++++++++++ .../ruilaizi/example/PlaceholderActivity.java | 34 ++++++ .../example/TravelPlanningActivity.java | 84 +++++++++++++ .../example/WeeklyReportActivity.java | 86 +++++++++++++ .../data/meeting/MeetingMinutesGenerator.java | 31 +++++ .../meeting/MeetingMinutesRepository.java | 20 +++ .../data/travel/TravelPlanGenerator.java | 30 +++++ .../data/travel/TravelPlanningRepository.java | 20 +++ .../data/weekly/WeeklyReportGenerator.java | 32 +++++ .../data/weekly/WeeklyReportRepository.java | 20 +++ .../example/ui/MeetingMinutesViewModel.java | 59 +++++++++ .../example/ui/TravelPlanningViewModel.java | 59 +++++++++ .../example/ui/WeeklyReportViewModel.java | 59 +++++++++ .../main/res/layout/activity_ai_services.xml | 56 +++++++++ .../app/src/main/res/layout/activity_main.xml | 4 +- .../res/layout/activity_meeting_minutes.xml | 93 ++++++++++++++ .../main/res/layout/activity_placeholder.xml | 43 +++++++ .../res/layout/activity_travel_planning.xml | 114 ++++++++++++++++++ .../res/layout/activity_weekly_report.xml | 62 ++++++++++ .../src/main/res/layout/item_ai_service.xml | 52 ++++++++ example/app/src/main/res/values/strings.xml | 4 + 24 files changed, 1180 insertions(+), 4 deletions(-) create mode 100644 example/app/src/main/java/com/ruilaizi/example/AIServicesActivity.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/MeetingMinutesActivity.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/PlaceholderActivity.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/TravelPlanningActivity.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/WeeklyReportActivity.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/meeting/MeetingMinutesGenerator.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/meeting/MeetingMinutesRepository.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/travel/TravelPlanGenerator.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/travel/TravelPlanningRepository.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/weekly/WeeklyReportGenerator.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/data/weekly/WeeklyReportRepository.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/ui/MeetingMinutesViewModel.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/ui/TravelPlanningViewModel.java create mode 100644 example/app/src/main/java/com/ruilaizi/example/ui/WeeklyReportViewModel.java create mode 100644 example/app/src/main/res/layout/activity_ai_services.xml create mode 100644 example/app/src/main/res/layout/activity_meeting_minutes.xml create mode 100644 example/app/src/main/res/layout/activity_placeholder.xml create mode 100644 example/app/src/main/res/layout/activity_travel_planning.xml create mode 100644 example/app/src/main/res/layout/activity_weekly_report.xml create mode 100644 example/app/src/main/res/layout/item_ai_service.xml 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 应用