Compare commits

...

2 Commits

Author SHA1 Message Date
xiaji
262920b8b3 feat: 优化三栏结果显示布局 2026-05-09 10:33:15 +08:00
xiaji
87cba41d01 docs: 更新README,添加多LLM并发功能说明 2026-05-09 10:23:16 +08:00
2 changed files with 106 additions and 85 deletions

View File

@@ -1,6 +1,6 @@
# Flomo AI
一个 AI 文本优化工具,包含 Android 原版和 Rust Windows 桌面移植版。
一个 AI 文本优化工具,支持 Android 原版和 Rust Windows 桌面版。
## 项目结构
@@ -11,7 +11,6 @@
│ │ │ └── com/example/flomo_ai/
│ │ │ ├── MainActivity.kt # 主界面
│ │ │ ├── SecondActivity.kt # 设置页面
│ │ │ ├── kwt.kt # JWT 生成工具
│ │ │ └── ui/theme/ # 主题管理
│ │ └── res/ # 布局、颜色、图标等资源
│ └── mumu-pytest/ # MuMu 模拟器自动化测试
@@ -26,10 +25,7 @@
│ │ └── llm_client.rs # LLM API 调用OpenAI 兼容格式)
│ ├── theme/
│ │ └── mod.rs # 明暗主题管理
── pages/
│ │ ├── main_page.rs # 主界面组件
│ │ └── settings_page.rs # 设置页组件
│ └── widgets/ # 可复用 UI 组件
── pages/ # 页面组件
├── .cargo/config.toml # MinGW 工具链配置
└── Cargo.toml # 依赖声明
```
@@ -37,7 +33,8 @@
## 功能特性
### 核心功能
- **多模型配置**:支持添加、编辑、删除多个大模型配置,首页可快速切换
- **多模型并发**:支持同时配置 3 个 LLM 模型,发送时并发调用所有启用的模型
- **独立测试**:每个模型可单独测试连接状态
- **LLM API 调用**:兼容 OpenAI 格式的 API支持自定义 Base URL、API Key、Model
- **自定义请求头**:可添加额外的 HTTP Header
- **提示词管理**:添加、删除、编辑系统提示词,主界面下拉选择
@@ -45,15 +42,18 @@
- **主题切换**:浅色模式 / 深色模式 / 跟随系统
- **配置持久化**JSON 格式保存到用户配置目录
- **复制结果**:一键复制优化结果到剪贴板
- **笔记保存**:支持 Flomo、Notion、Joplin 等笔记 APIAPI Key 可选)
### 界面布局
- **顶部标题栏**:显示 "AI优化" + 模型选择下拉框 + 配置按钮
- **快速操作区**:四个 emoji 快捷按钮
- **提示词选择器**:下拉菜单选择系统提示词,显示提示词内容预览
- **输入区域**多行文本输入自动增长3-10行限制
- **发送/停止**:发送按钮 + 生成中可停止
- **结果展示区**:状态标签 + 可编辑结果文本 + 复制按钮 + 保存笔记按钮
- **主界面**
- 顶部标题栏 + 配置按钮
- 提示词选择器 + 快速操作按钮
- 输入区域 + 发送按钮
- **三栏结果显示**:水平排列 3 个模型的结果卡片
- **配置页面**
- 3 个模型的独立配置启用开关、名称、URL、Key、Model
- 每个模型独立的测试按钮
- 主题设置
- 提示词管理
## 构建说明
@@ -74,7 +74,9 @@ cd flomo-ai-desktop
cargo build --release
```
编译产物:`target/release/flomo-ai.exe`~4.4MB,无控制台窗口)
或使用 `build.bat`(需先安装 MSYS2 + MinGW
编译产物:`target/release/flomo-ai.exe`~4MB无控制台窗口
### 编译配置
- `.cargo/config.toml`:指定 MinGW 链接器和 `-mwindows` 参数
@@ -83,8 +85,8 @@ cargo build --release
### 依赖
| 依赖 | 用途 |
|------|------|
| `egui` + `eframe` | GUI 框架(纯 Rust无 C 依赖) |
| `reqwest` (blocking + rustls-tls) | HTTP 客户端(纯 Rust TLS |
| `egui` + `eframe` (glow) | GUI 框架(纯 Rust无 C 依赖) |
| `reqwest` (blocking + native-tls) | HTTP 客户端 |
| `serde` + `serde_json` | 序列化 / 配置持久化 |
| `dirs` | 获取用户配置目录路径 |
| `arboard` | 剪贴板操作 |
@@ -95,10 +97,30 @@ cargo build --release
```json
{
"llm_config": {
"base_url": "https://api.openai.com/v1",
"api_key": "",
"model": "gpt-4o"
"llm_configs": {
"models": [
{
"enabled": true,
"name": "模型1",
"base_url": "https://api.openai.com/v1",
"api_key": "",
"model": "gpt-4o"
},
{
"enabled": false,
"name": "模型2",
"base_url": "",
"api_key": "",
"model": ""
},
{
"enabled": false,
"name": "模型3",
"base_url": "",
"api_key": "",
"model": ""
}
]
},
"header_configs": [],
"prompt_configs": [

View File

@@ -288,79 +288,78 @@ impl FlomoAiApp {
ui.add_space(6.0);
let available_width = ui.available_width();
let column_width = (available_width - 10.0) / 3.0;
let column_width = (available_width - 16.0) / 3.0;
ui.horizontal(|ui| {
ui.set_height(200.0);
for (i, display) in self.model_displays.iter().enumerate() {
if i > 0 {
ui.add_space(5.0);
ui.add_space(8.0);
}
ui.set_min_width(column_width);
ui.set_max_width(column_width);
ui.vertical(|ui| {
ui.set_width(column_width);
egui::Frame::none()
.fill(ui.style().visuals.widgets.inactive.bg_fill)
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(180, 180, 200)))
.rounding(8.0)
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
ui.set_min_width(column_width - 20.0);
let bg_color = if display.enabled {
ui.style().visuals.widgets.inactive.bg_fill
} else {
egui::Color32::from_rgb(240, 240, 240)
};
let enabled_color = if display.enabled {
egui::Color32::from_rgb(80, 80, 220)
} else {
egui::Color32::GRAY
};
egui::Frame::none()
.fill(bg_color)
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 210)))
.rounding(6.0)
.inner_margin(egui::Margin::same(8.0))
.show(ui, |ui| {
let status_color = match &display.status {
ModelStatus::Waiting => egui::Color32::GRAY,
ModelStatus::Loading => egui::Color32::from_rgb(255, 165, 0),
ModelStatus::Completed => egui::Color32::from_rgb(0, 180, 0),
ModelStatus::Error(_) => egui::Color32::RED,
};
let enabled_dot = if display.enabled { "" } else { "" };
ui.label(egui::RichText::new(format!("{} {}", enabled_dot, display.name))
.size(13.0).strong().color(enabled_color));
let enabled_dot = if display.enabled { "" } else { "" };
ui.label(egui::RichText::new(format!("{} {}", enabled_dot, display.name))
.size(12.0).strong()
.color(if display.enabled { egui::Color32::from_rgb(100, 100, 255) } else { egui::Color32::GRAY }));
ui.label(egui::RichText::new(&display.model).size(10.0).color(egui::Color32::GRAY));
ui.label(egui::RichText::new(&display.model).size(10.0).color(egui::Color32::GRAY));
ui.add_space(6.0);
ui.add_sized([ui.available_width(), 1.0], egui::Separator::default());
ui.add_space(4.0);
ui.add_sized([ui.available_width(), 1.0], egui::Separator::default());
ui.add_space(4.0);
ui.add_space(6.0);
if display.result.is_empty() {
ui.label(egui::RichText::new("等待结果...").size(11.0).color(egui::Color32::GRAY));
} else {
ui.add_sized(
[ui.available_width(), 120.0],
egui::TextEdit::multiline(&mut display.result.clone())
.desired_rows(5)
.frame(false),
);
let status_color = match &display.status {
ModelStatus::Waiting => egui::Color32::GRAY,
ModelStatus::Loading => egui::Color32::from_rgb(255, 140, 0),
ModelStatus::Completed => egui::Color32::from_rgb(0, 160, 0),
ModelStatus::Error(_) => egui::Color32::RED,
};
ui.label(egui::RichText::new(match &display.status {
ModelStatus::Waiting => "● 就绪",
ModelStatus::Loading => "◐ 生成中...",
ModelStatus::Completed => "✓ 完成",
ModelStatus::Error(_) => "✗ 错误",
}).size(10.0).color(status_color));
ui.add_space(4.0);
if display.result.is_empty() {
ui.label(egui::RichText::new("等待结果...").size(11.0).color(egui::Color32::GRAY));
} else {
let mut result_text = display.result.clone();
ui.add_sized(
[ui.available_width(), 120.0],
egui::TextEdit::multiline(&mut result_text)
.desired_rows(5)
.frame(false),
);
}
ui.add_space(6.0);
ui.horizontal(|ui| {
if ui.small_button("复制").clicked() && !display.result.is_empty() {
self.copy_to_clipboard(&display.result);
}
ui.add_space(4.0);
ui.add_sized([ui.available_width(), 1.0], egui::Separator::default());
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new(match &display.status {
ModelStatus::Waiting => "就绪",
ModelStatus::Loading => "生成中...",
ModelStatus::Completed => "完成",
ModelStatus::Error(_) => "错误",
}).size(10.0).color(status_color));
if ui.small_button("复制").clicked() && !display.result.is_empty() {
self.copy_to_clipboard(&display.result);
}
});
});
});
});
}
});
});