Compare commits

..

10 Commits

95 changed files with 5194 additions and 658 deletions

3
.fvmrc Normal file
View File

@@ -0,0 +1,3 @@
{
"flutter": "3.27.4"
}

5
.gitignore vendored
View File

@@ -43,3 +43,8 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
.vscode/
# FVM Version Cache
.fvm/

644
CODE_ANALYSIS.md Normal file
View File

@@ -0,0 +1,644 @@
# AI Chat Assistant Flutter Plugin - 详细代码逻辑文档
## 📋 项目概述
这是一个专为车载系统设计的AI聊天助手Flutter插件采用插件化架构设计支持语音识别、AI对话、车控命令执行等功能。项目从原有的App结构重构为Plugin结构便于集成到其他Flutter应用中。
## 🏗️ 核心架构分析
### 1. 架构模式
- **插件化架构**: 采用Flutter Plugin架构便于第三方应用集成
- **单例模式**: 核心服务类(MessageService)采用单例模式确保全局状态一致
- **观察者模式**: 基于Provider的状态管理实现响应式UI更新
- **回调机制**: 通过CommandCallback实现车控命令的解耦处理
### 2. 目录结构与职责
```
lib/
├── app.dart # 插件主入口,提供初始化接口
├── enums/ # 枚举定义层
├── models/ # 数据模型层
├── services/ # 业务逻辑服务层
├── screens/ # UI界面层
├── widgets/ # UI组件层
├── utils/ # 工具类层
└── themes/ # 主题样式层
```
## 🔧 核心枚举定义 (Enums)
### MessageServiceState
```dart
enum MessageServiceState {
idle, // 空闲状态
recording, // 录音中
recognizing, // 识别中
replying, // 回复中
}
```
### MessageStatus
```dart
enum MessageStatus {
normal, // 普通消息
listening, // 聆听中
recognizing, // 识别中
thinking, // 思考中
completed, // 完成回答
executing, // 执行中
success, // 执行成功
failure, // 执行失败
aborted, // 已中止
}
```
### VehicleCommandType
支持22种车控命令类型
- 车门控制lock, unlock
- 车窗控制openWindow, closeWindow
- 空调控制openAC, closeAC, changeACTemp, coolSharply
- 特殊功能prepareCar, meltSnow, honk, locateCar
- 加热功能:座椅加热、方向盘加热等
## 📊 数据模型层 (Models)
### ChatMessage
```dart
class ChatMessage {
final String id; // 消息唯一标识
final String text; // 消息内容
final bool isUser; // 是否为用户消息
final DateTime timestamp; // 时间戳
MessageStatus status; // 消息状态
}
```
### VehicleCommand
```dart
class VehicleCommand {
final VehicleCommandType type; // 命令类型
final Map<String, dynamic>? params; // 命令参数
final String error; // 错误信息
}
```
## 🔄 核心服务层 (Services) - 完整解析
### 1. MessageService - 核心消息管理服务
**设计模式**: 单例模式 + 观察者模式
**主要职责**:
- 统一管理聊天消息
- 协调语音识别、AI对话、车控命令执行流程
- 维护应用状态机
- 通过Method Channel与原生ASR服务通信
**核心方法分析**:
#### 语音输入流程
```dart
// 开始语音输入
Future<void> startVoiceInput() async {
// 1. 权限检查 - 使用permission_handler检查麦克风权限
// 2. 状态验证 - 确保当前状态为idle
// 3. 初始化录音 - 创建用户消息占位符
// 4. 调用原生ASR服务 - 通过Method Channel启动阿里云ASR
}
// 停止并处理语音输入
Future<void> stopAndProcessVoiceInput() async {
// 1. 停止录音 - 调用原生ASR停止接口
// 2. 等待ASR结果 - 使用Completer等待异步结果
// 3. 调用reply()处理识别结果
}
```
#### 智能回复流程
```dart
Future<void> reply(String text) async {
// 1. 文本分类(TextClassificationService)
// 2. 根据分类结果路由到不同处理逻辑:
// - 车控命令 (category=2) -> handleVehicleControl()
// - 普通问答 (default) -> answerQuestion()
// - 错误问题 (category=4) -> answerWrongQuestion()
// - 系统错误 (category=-1) -> occurError()
}
```
#### 车控命令处理
```dart
Future<void> handleVehicleControl(String text, bool isChinese) async {
// 1. 调用VehicleCommandService解析命令
// 2. 执行TTS播报
// 3. 逐个执行车控命令
// 4. 处理命令执行结果
// 5. 生成执行反馈
}
```
**状态管理机制**:
- 使用`ChangeNotifier`实现响应式状态更新
- 通过`_state`维护服务状态机
- 使用`_isReplyAborted`实现流程中断控制
### 2. ChatSseService - SSE流式通信服务
**设计模式**: 单例服务 + 流式处理
**核心功能**:
- 基于SSE的实时AI对话
- 流式文本处理和TTS播报
- 会话状态管理
- 智能分句和语音合成
**关键实现**:
#### SSE连接管理
```dart
void request({
required String messageId,
required String text,
required bool isChinese,
required Function(String, String, bool) onStreamResponse,
}) async {
// 1. 初始化用户ID和会话ID
// 2. 建立HTTP SSE连接
// 3. 处理流式响应数据
// 4. 实时文本清理和分句
// 5. 协调TTS播报
}
```
#### 智能分句算法
```dart
// 支持中英文分句符识别
final enEnders = RegExp(r'[.!?]');
final zhEnders = RegExp(r'[。!?]');
// Markdown内容清理
// 1. 清理图片语法 ![...]
// 2. 移除列表序号
// 3. 处理不完整语法
```
### 3. AudioRecorderService - 音频录制服务
**核心功能**:
- 基于record插件的音频录制
- OPUS格式音频编码
- 权限管理
- 临时文件管理
**实现细节**:
```dart
class AudioRecorderService {
final AudioRecorder _recorder = AudioRecorder();
bool _isRecording = false;
String? _tempFilePath;
Future<void> startRecording() async {
// 1. 生成临时文件路径
// 2. 配置OPUS编码格式
// 3. 启动录音
}
Future<List<int>?> stopRecording() async {
// 1. 停止录音
// 2. 读取音频文件
// 3. 返回字节数组
}
}
```
### 4. VoiceRecognitionService - 语音识别服务
**功能**:
- HTTP多媒体文件上传
- 音频格式转换
- 中英文语言识别
**核心逻辑**:
```dart
Future<String?> recognizeSpeech(List<int> audioBytes, {String lang = 'cn'}) async {
// 1. 构建MultipartRequest
// 2. 上传音频文件到识别服务
// 3. 解析JSON响应获取文本
// 4. 错误处理和日志记录
}
```
### 5. LocalTtsService - 语音合成服务
**设计亮点**: 流式TTS处理 + 有序播放队列
**核心特性**:
- 基于flutter_tts的跨平台TTS
- 流式文本分句播报
- 有序任务队列管理
- 中英文语言自动切换
**关键数据结构**:
```dart
class TtsTask {
final int index; // 任务索引
final String text; // 播报文本
TtsTaskStatus status; // 任务状态(ready/completed)
}
enum TtsTaskStatus {
ready, // 准备播放
completed, // 播放完成
}
```
**流式播放流程**:
```dart
void pushTextForStreamTTS(String text) {
// 1. 创建有序任务
// 2. 添加到任务队列
// 3. 触发下一个任务处理
}
void _processNextReadyTask() {
// 1. 检查播放状态
// 2. 按序查找就绪任务
// 3. 执行TTS播放
// 4. 等待播放完成回调
}
```
### 6. TextClassificationService - 文本分类服务
**功能**: NLP文本意图分类
**分类结果**:
- `-1`: 系统错误
- `2`: 车控命令
- `4`: 无效问题
- 其他: 普通问答
```dart
Future<int> classifyText(String text) async {
// HTTP POST到分类服务
// 返回分类category整数
}
```
### 7. VehicleCommandService - 车控命令解析服务
**职责**:
- 自然语言转车控命令
- 命令参数解析
- 执行结果反馈生成
**核心方法**:
```dart
Future<VehicleCommandResponse?> getCommandFromText(String text) async {
// 1. 发送文本到NLP解析服务
// 2. 解析返回的命令列表
// 3. 构建VehicleCommandResponse对象
}
Future<String> getControlResponse(List<String> successCommandList) async {
// 根据成功命令列表生成友好反馈文本
}
```
### 8. CommandService - 车控命令执行服务
**设计模式**: 静态方法 + 回调机制
**解耦设计**:
```dart
typedef CommandCallback = Future<(bool, Map<String, dynamic>? params)> Function(
VehicleCommandType type, Map<String, dynamic>? params);
static Future<(bool, Map<String, dynamic>? params)> executeCommand(
VehicleCommandType type, {Map<String, dynamic>? params}) async {
// 调用主应用注册的回调函数
// 返回执行结果元组(成功状态, 返回参数)
}
```
### 9. RedisService - 缓存服务
**功能**: 基于HTTP的Redis缓存操作
```dart
class RedisService {
Future<void> setKeyValue(String key, Object value) async {
// HTTP POST设置键值对
}
Future<String?> getValue(String key) async {
// HTTP GET获取值
}
}
```
### 10. LocationService - 位置服务
用于车辆定位相关功能(具体实现需查看完整代码)
### 11. VehicleStateService - 车辆状态服务
**注释代码分析**:
- 原设计基于InGeek MDK车载SDK
- 支持车辆状态实时监听
- 与Redis服务配合缓存状态数据
- 当前版本已注释,可能在插件化过程中移除
## 🎨 UI层架构分析
### 1. 主界面 (MainScreen)
**设计**: 采用Stack布局背景图片 + 浮动图标
```dart
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// 背景图片
Container(decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/bg.jpg', package: 'ai_chat_assistant'),
),
)),
// 浮动AI助手图标
FloatingIcon(),
],
),
);
}
```
### 2. 浮动图标 (FloatingIcon)
**功能特性**:
- 可拖拽定位
- 状态指示(通过不同图标)
- 点击展开聊天界面
- 动画效果支持
**交互流程**:
1. 长按拖拽调整位置
2. 点击展开部分屏幕聊天界面
3. 根据MessageService状态切换图标
### 3. 聊天界面架构
**部分屏模式** (PartScreen):
- 简化的聊天界面
- 支持语音输入和文本显示
- 可展开到全屏模式
**全屏模式** (FullScreen):
- 完整的聊天功能
- 历史消息展示
- 更多操作按钮
## 🔌 原生平台集成
### Android ASR集成
**Method Channel通信**:
```dart
static const MethodChannel _asrChannel =
MethodChannel('com.example.ai_chat_assistant/ali_sdk');
```
**ASR事件处理**:
```dart
_asrChannel.setMethodCallHandler((call) async {
switch (call.method) {
case "onAsrResult":
// 实时更新识别结果
replaceMessage(id: _latestUserMessageId!, text: call.arguments);
break;
case "onAsrStop":
// 识别结束,触发后续处理
if (_asrCompleter != null && !_asrCompleter!.isCompleted) {
_asrCompleter!.complete(messages.last.text);
}
break;
}
});
```
## 🛠️ 工具类分析
### CommonUtil
**主要功能**:
- 中文检测: `containChinese(String text)`
- Markdown清理: `cleanText(String text, bool forTts)`
- 公共颜色常量定义
**Markdown清理逻辑**:
```dart
static String cleanText(String text, bool forTts) {
// 1. 清理粗体/斜体标记 **text** *text*
// 2. 处理代码块 ```code```
// 3. 清理表格格式 |---|---|
// 4. 处理链接和图片 [text](url) ![alt](src)
// 5. 清理列表和引用 1. - * >
// 6. 规范化空白字符
}
```
## 🔄 数据流分析
### 完整对话流程
```mermaid
graph TD
A[用户点击语音按钮] --> B[startVoiceInput]
B --> C[检查麦克风权限]
C --> D[开始录音/ASR]
D --> E[实时显示识别结果]
E --> F[用户松开按钮]
F --> G[stopAndProcessVoiceInput]
G --> H[调用reply处理文本]
H --> I[文本分类]
I --> J{分类结果}
J -->|车控命令| K[handleVehicleControl]
J -->|普通问答| L[answerQuestion - ChatSseService]
J -->|错误问题| M[answerWrongQuestion]
K --> N[解析车控命令]
N --> O[执行TTS播报]
O --> P[执行车控命令]
P --> Q[生成执行反馈]
Q --> R[更新UI显示]
L --> S[SSE流式对话]
S --> T[实时TTS播报]
T --> R
```
### 服务间协作关系
```
MessageService (核心协调器)
├── ChatSseService (AI对话)
│ └── LocalTtsService (流式TTS)
├── AudioRecorderService (音频录制)
├── VoiceRecognitionService (语音识别)
├── TextClassificationService (文本分类)
├── VehicleCommandService (车控解析)
│ └── CommandService (命令执行)
└── RedisService (状态缓存)
```
### 状态管理流程
**MessageService状态变化**:
```
idle -> recording -> recognizing -> replying -> idle
```
**消息状态变化**:
```
listening -> normal -> thinking -> executing -> success/failure
```
**TTS任务状态**:
```
ready -> playing -> completed
```
## 🔧 配置与扩展
### 1. 插件初始化
```dart
// 在主应用中初始化插件
ChatAssistantApp.initialize(
commandCallback: (VehicleCommandType type, Map<String, dynamic>? params) async {
// 实现具体的车控逻辑
return (true, {'message': '命令执行成功'});
},
);
```
### 2. 服务端接口配置
**当前硬编码的服务端地址**:
- 文本分类: `http://143.64.185.20:18606/classify`
- 车控解析: `http://143.64.185.20:18606/control`
- 控制反馈: `http://143.64.185.20:18606/control_resp`
- SSE对话: `http://143.64.185.20:18606/chat`
- 语音识别: `http://143.64.185.20:18606/voice`
- Redis缓存: `http://143.64.185.20:18606/redis/*`
**建议改进**: 将服务端地址配置化,支持动态配置
### 3. 资源文件引用
**图片资源**:
- 使用`package: 'ai_chat_assistant'`前缀
- 路径: `assets/images/`
**字体资源**:
- VWHead_Bold.otf
- VWHead_Regular.otf
## 🐛 已知问题与注意事项
### 1. 资源路径问题
当前资源文件引用可能存在路径问题需要确保在example项目中正确配置package前缀。
### 2. 硬编码服务地址
服务端API地址硬编码不利于部署和环境切换。
### 3. 错误处理
部分网络请求缺少完善的错误处理和重试机制。
### 4. 内存管理
长时间运行可能存在内存泄漏风险,需要关注:
- Completer的正确释放
- HTTP连接的及时关闭
- TTS任务队列的清理
### 5. 并发控制
- SSE连接的并发控制
- TTS播放队列的线程安全
- 语音识别状态的同步
## 🚀 扩展建议
### 1. 配置化改进
- 服务端地址配置化
- 支持多语言配置
- 主题自定义配置
- TTS引擎选择配置
### 2. 功能增强
- 添加离线语音识别支持
- 实现消息历史持久化
- 添加更多车控命令类型
- 支持自定义唤醒词
### 3. 性能优化
- 实现网络请求缓存
- 优化UI渲染性能
- 添加资源预加载
- TTS队列优化
### 4. 测试完善
- 添加单元测试覆盖所有服务
- 集成测试覆盖完整流程
- 性能测试工具
- 错误场景测试
### 5. 架构优化
- 依赖注入容器
- 事件总线机制
- 插件热重载支持
- 模块化拆分
## 📝 开发流程建议
### 1. 添加新功能
1. 在相应的枚举中定义新类型
2. 在models中添加数据模型
3. 在services中实现业务逻辑
4. 在widgets中创建UI组件
5. 更新主流程集成新功能
### 2. 调试技巧
- 使用`debugPrint`添加关键节点日志
- 通过Provider DevTools监控状态变化
- 使用Flutter Inspector检查UI结构
- 监控Method Channel通信日志
### 3. 集成测试
- 在example项目中测试完整流程
- 验证车控命令回调机制
- 测试不同场景下的错误处理
- 验证资源文件引用
## 💡 技术亮点总结
### 1. 流式处理架构
- SSE实时对话流
- TTS流式播报
- 有序任务队列管理
### 2. 智能语音处理
- 实时语音识别显示
- 中英文自动检测
- 智能分句算法
### 3. 解耦设计
- 插件与主应用解耦
- 服务间松耦合
- 回调机制灵活扩展
### 4. 状态管理
- 响应式UI更新
- 多层状态协调
- 异步状态同步
---

421
README.md
View File

@@ -1,16 +1,417 @@
# app003 # AI Chat Assistant Flutter Plugin
A new Flutter project. 一个功能丰富的 AI 聊天助手 Flutter 插件专为车载系统设计支持语音识别、AI 对话、车控命令执行、语音合成等功能。
## Getting Started ## 📱 功能特性
This project is a starting point for a Flutter application. - 🎤 **语音识别 (ASR)**: 支持实时语音转文字,自动停止检测
- 🤖 **AI 对话**: 基于 SSE (Server-Sent Events) 的流式 AI 对话
- 🚗 **车控命令**: 支持 20+ 种车辆控制命令(空调、车窗、车门、座椅加热等)
- 🔊 **语音合成 (TTS)**: 支持中英文语音播报
- 🎨 **精美 UI**: 浮动图标、波纹动画、渐变背景
- 📱 **多界面模式**: 支持全屏和部分屏幕聊天界面
- 🌐 **国际化**: 支持中英文切换
- 📊 **状态管理**: 基于 Provider 的状态管理
A few resources to get you started if this is your first Flutter project: ## 🏗️ 项目架构
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) ### 📁 目录结构
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the ```
[online documentation](https://docs.flutter.dev/), which offers tutorials, lib/
samples, guidance on mobile development, and a full API reference. ├── app.dart # 主应用入口
├── main.dart # 程序启动入口
├── enums/ # 枚举定义
│ ├── message_service_state.dart # 消息服务状态
│ ├── message_status.dart # 消息状态
│ └── vehicle_command_type.dart # 车控命令类型
├── models/ # 数据模型
│ ├── chat_message.dart # 聊天消息模型
│ ├── vehicle_cmd.dart # 车控命令模型
│ ├── vehicle_cmd_response.dart # 车控命令响应
│ └── vehicle_status_info.dart # 车辆状态信息
├── screens/ # 界面页面
│ ├── full_screen.dart # 全屏聊天界面
│ ├── main_screen.dart # 主界面
│ └── part_screen.dart # 部分屏聊天界面
├── services/ # 核心服务
│ ├── audio_recorder_service.dart # 音频录制服务
│ ├── chat_sse_service.dart # SSE 聊天服务
│ ├── classification_service.dart # 分类服务
│ ├── command_service.dart # 车控命令服务
│ ├── control_recognition_service.dart # 控制识别服务
│ ├── location_service.dart # 位置服务
│ ├── message_service.dart # 消息管理服务
│ ├── redis_service.dart # Redis 服务
│ ├── tts_service.dart # 语音合成服务
│ ├── vehicle_state_service.dart # 车辆状态服务
│ └── voice_recognition_service.dart # 语音识别服务
├── themes/ # 主题样式
│ └── AppTheme.dart # 应用主题定义
├── utils/ # 工具类
│ ├── common_util.dart # 通用工具
│ ├── tts_engine_manager.dart # TTS 引擎管理
│ └── tts_util.dart # TTS 工具
└── widgets/ # UI 组件
├── assistant_avatar.dart # AI 助手头像
├── chat_box.dart # 聊天框
├── chat_bubble.dart # 聊天气泡
├── chat_footer.dart # 聊天底部
├── chat_header.dart # 聊天头部
├── floating_icon.dart # 浮动图标
├── floating_icon_with_wave.dart # 带波纹的浮动图标
├── gradient_background.dart # 渐变背景
└── large_audio_wave.dart # 大音频波形
```
### 🎯 核心架构设计
#### 1. 服务层架构 (Services Layer)
- **MessageService**: 核心消息管理服务负责统一管理聊天消息、语音识别、AI 对话流程
- **ChatSseService**: SSE 流式通信服务,处理与 AI 后端的实时对话
- **CommandService**: 车控命令处理服务,提供车辆控制命令的回调机制
- **TTSService**: 语音合成服务,支持中英文语音播报
- **VoiceRecognitionService**: 语音识别服务,处理音频转文字
#### 2. UI 层架构 (UI Layer)
- **浮动图标模式**: 主界面显示可拖拽的 AI 助手浮动图标
- **部分屏幕模式**: 点击图标显示简化聊天界面
- **全屏模式**: 完整的聊天界面,支持历史记录、操作按钮等
#### 3. 状态管理 (State Management)
基于 Provider 模式的响应式状态管理:
- 消息状态管理
- 语音识别状态
- AI 对话状态
- 车控命令执行状态
## 🚗 支持的车控命令
| 命令类型 | 中文描述 | 英文描述 |
|---------|---------|---------|
| lock | 上锁车门 | lock |
| unlock | 解锁车门 | unlock |
| openWindow | 打开车窗 | open window |
| closeWindow | 关闭车窗 | close window |
| appointAC | 预约空调 | appoint AC |
| openAC | 打开空调 | open AC |
| closeAC | 关闭空调 | close AC |
| changeACTemp | 修改空调温度 | change AC temperature |
| coolSharply | 极速降温 | cool sharply |
| prepareCar | 一键备车 | prepare car |
| meltSnow | 一键融雪 | melt snow |
| openTrunk | 打开后备箱 | open trunk |
| closeTrunk | 关闭后备箱 | close trunk |
| honk | 鸣笛 | honk |
| locateCar | 定位车辆 | locate car |
| openWheelHeat | 开启方向盘加热 | open wheel heat |
| closeWheelHeat | 关闭方向盘加热 | close wheel heat |
| openMainSeatHeat | 开启主座椅加热 | open main seat heat |
| closeMainSeatHeat | 关闭主座椅加热 | close main seat heat |
| openMinorSeatHeat | 开启副座椅加热 | open minor seat heat |
| closeMinorSeatHeat | 关闭副座椅加热 | close minor seat heat |
## 🔧 技术栈
### 核心依赖
- **Flutter SDK**: ^3.6.2
- **Provider**: ^6.1.5 - 状态管理
- **HTTP**: ^1.4.0 - 网络请求
- **Record**: ^6.0.0 - 音频录制
- **AudioPlayers**: ^5.2.1 - 音频播放
- **Flutter TTS**: ^4.2.0 - 语音合成
- **Permission Handler**: ^12.0.0 - 权限管理
- **UUID**: ^3.0.5 - 唯一标识符生成
- **Flutter Markdown**: ^0.7.7+1 - Markdown 渲染
- **FlutterToast**: ^8.2.12 - 消息提示
- **Path Provider**: ^2.1.5 - 文件路径管理
### 自定义包
- **basic_intl**: 基础国际化支持包
## 🚀 快速开始
### 1. 添加依赖
在您的 `pubspec.yaml` 文件中添加:
```yaml
dependencies:
ai_chat_assistant:
path: path/to/ai_chat_assistant
```
### 2. 权限配置
#### Android 权限 (android/app/src/main/AndroidManifest.xml)
```xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
```
### 3. 基础集成
```dart
import 'package:flutter/material.dart';
import 'package:ai_chat_assistant/app.dart';
import 'package:ai_chat_assistant/services/message_service.dart';
import 'package:ai_chat_assistant/enums/vehicle_command_type.dart';
import 'package:provider/provider.dart';
import 'package:permission_handler/permission_handler.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 请求麦克风权限
if (!await Permission.microphone.isGranted) {
await Permission.microphone.request();
}
// 初始化 AI Chat Assistant注册车控命令回调
ChatAssistantApp.initialize(
commandCallback: (VehicleCommandType type, Map<String, dynamic>? params) async {
// 处理车控命令的业务逻辑
print('收到车控命令: $type, 参数: $params');
// 返回执行结果
return (true, {'message': '命令已执行'});
},
);
runApp(
ChangeNotifierProvider(
create: (_) => MessageService(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My Car App',
home: const ChatAssistantApp(), // 直接使用 AI 助手
);
}
}
```
### 4. 自定义集成
如果您想在现有应用中集成聊天助手,可以使用浮动图标:
```dart
import 'package:ai_chat_assistant/widgets/floating_icon.dart';
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// 您的现有 UI
YourExistingWidget(),
// AI 助手浮动图标
const FloatingIcon(),
],
),
);
}
}
```
## 📖 示例项目
项目包含完整的示例应用,位于 `example/` 目录下:
```bash
cd example
flutter run
```
示例展示了:
- 基础集成方法
- 车控命令处理
- 自定义 UI 主题
- 权限管理
## 🎨 UI 组件
### 浮动图标 (FloatingIcon)
可拖拽的 AI 助手图标,支持:
- 拖拽定位
- 点击展开聊天界面
- 动态图标切换
- 波纹动画效果
### 聊天界面组件
- **ChatBubble**: 聊天气泡组件
- **ChatBox**: 聊天输入框
- **ChatHeader**: 聊天头部
- **ChatFooter**: 聊天底部操作栏
- **AssistantAvatar**: AI 助手头像
### 动画组件
- **FloatingIconWithWave**: 带波纹效果的浮动图标
- **LargeAudioWave**: 大音频波形动画
- **GradientBackground**: 渐变背景
## ⚙️ 配置选项
### 主题自定义
```dart
// 在 AppTheme.dart 中自定义主题
class AppTheme {
static ThemeData get lightTheme => ThemeData(
// 自定义浅色主题
);
static ThemeData get darkTheme => ThemeData(
// 自定义深色主题
);
}
```
### 资源文件
确保在 `pubspec.yaml` 中包含必要的资源:
```yaml
flutter:
assets:
- assets/images/
fonts:
- family: VWHead_Bold
fonts:
- asset: assets/fonts/VWHead-Bold.otf
- family: VWHead_Regular
fonts:
- asset: assets/fonts/VWHead-Regular.otf
```
## 🔌 Native 平台集成
### Android 平台
项目包含 Android 原生代码,支持:
- ASR (自动语音识别) 功能
- 原生音频处理
- 系统权限管理
### 插件架构
```yaml
plugin:
platforms:
android:
package: com.example.ai_assistant_plugin
pluginClass: AiAssistantPlugin
```
## 📝 API 文档
### MessageService
核心消息管理服务:
```dart
class MessageService extends ChangeNotifier {
// 发送消息
Future<void> sendMessage(String text);
// 开始语音识别
Future<void> startVoiceRecognition();
// 停止语音识别
void stopVoiceRecognition();
// 获取聊天历史
List<ChatMessage> get messages;
}
```
### CommandService
车控命令服务:
```dart
class CommandService {
// 注册命令回调
static void registerCallback(CommandCallback callback);
// 执行车控命令
static Future<(bool, Map<String, dynamic>?)> executeCommand(
VehicleCommandType type,
{Map<String, dynamic>? params}
);
}
```
## 🐛 调试和故障排除
### 常见问题
1. **麦克风权限问题**
- 确保在 AndroidManifest.xml 中添加了录音权限
- 在应用启动时请求权限
2. **网络连接问题**
- 检查网络权限配置
- 确认 SSE 服务端地址配置正确
3. **TTS 播放问题**
- 检查音频播放权限
- 确认设备支持 TTS 功能
### 日志调试
启用详细日志:
```dart
// 在 main.dart 中启用调试日志
void main() {
debugPrint('AI Chat Assistant 启动中...');
// ... 其他初始化代码
}
```
## 🤝 贡献指南
欢迎提交 Issue 和 Pull Request
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 打开 Pull Request
## 📄 许可证
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
## 🔗 相关链接
- [Flutter 官方文档](https://flutter.dev/docs)
- [Provider 状态管理](https://pub.dev/packages/provider)
- [Flutter TTS](https://pub.dev/packages/flutter_tts)
- [Record 插件](https://pub.dev/packages/record)
---
**注意**: 本插件专为车载系统设计,某些功能可能需要特定的硬件环境支持。

127
README_ChatPopup.md Normal file
View File

@@ -0,0 +1,127 @@
# ChatFloatingIcon 和 ChatPopup 组件说明
这两个组件用于替代原来的 `FloatingIcon`,解决了在 Stack 中会被上层遮挡的问题。
## 组件结构
### ChatFloatingIcon
可拖拽的悬浮图标组件,支持:
- 自定义图标和大小
- 拖拽功能(自动吸附屏幕边缘)
- 长按开始语音输入
- 点击进入全屏聊天
- 多种动画效果
### ChatPopup
基于 Overlay 的弹窗容器,负责:
- 管理 PartScreen 的显示/隐藏
- 根据图标位置自动计算弹窗位置
- 处理背景点击关闭
- 动画效果管理
## 使用方法
### 基本使用默认AI图标
```dart
ChatPopup(
child: ChatFloatingIcon(
iconSize: 80, // 可选默认80
),
),
```
### 自定义图标
```dart
ChatPopup(
child: ChatFloatingIcon(
iconSize: 60,
icon: Container(
width: 60,
height: 60,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: const Icon(Icons.chat, color: Colors.white),
),
),
),
```
### 添加自定义回调
```dart
ChatPopup(
child: ChatFloatingIcon(
iconSize: 70,
onTap: () {
// 自定义点击事件
},
onPositionChanged: (position) {
// 位置变化回调
},
onDragStart: () {
// 拖拽开始
},
onDragEnd: () {
// 拖拽结束
},
),
),
```
## 参数说明
### ChatFloatingIcon 参数
- `iconSize`: 图标大小默认80
- `icon`: 自定义图标Widget不传则使用默认AI图标
- `onTap`: 点击回调,默认进入全屏聊天
- `onLongPress`: 长按开始回调,默认开始语音输入
- `onLongPressUp`: 长按结束回调,默认结束语音输入
- `onLongPressEnd`: 长按结束详细回调
- `onDragStart`: 拖拽开始回调
- `onDragEnd`: 拖拽结束回调
- `onPositionChanged`: 位置变化回调
### ChatPopup 参数
- `child`: 子组件通常是ChatFloatingIcon
- `chatSize`: 聊天框大小默认320x450
## 功能特性
1. **避免遮挡**: 使用 Overlay 显示弹窗,不会被 Stack 上层遮挡
2. **位置自适应**: 根据图标位置自动计算弹窗显示位置
3. **保持原有逻辑**: 与原 FloatingIcon 功能完全兼容
4. **灵活自定义**: 支持自定义图标、回调等
5. **动画丰富**: 保持原有的各种动画效果
## 迁移指南
### 从 FloatingIcon 迁移
原来的代码:
```dart
Stack(
children: [
// 其他内容
FloatingIcon(),
],
)
```
新的代码:
```dart
Stack(
children: [
// 其他内容
ChatPopup(
child: ChatFloatingIcon(),
),
],
)
```
## 注意事项
1. ChatPopup 必须在有 Overlay 的 Widget 树中使用(通常是 MaterialApp 下)
2. 如果要自定义图标,建议保持圆形设计以匹配拖拽动画
3. 长按语音输入功能需要 MessageService 正确配置
4. 位置计算基于屏幕坐标,确保在正确的上下文中使用

2
android/.gitignore vendored
View File

@@ -12,3 +12,5 @@ GeneratedPluginRegistrant.java
key.properties key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks
/build/
/src/build/

112
android/build.gradle Normal file
View File

@@ -0,0 +1,112 @@
group 'com.example.ai_chat_assistant'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '2.1.20'
repositories {
maven { url file("mavenLocal") }
google()
mavenCentral()
maven {
url "https://storage.googleapis.com/download.flutter.io"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:8.11.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
maven { url file("mavenLocal") }
google()
mavenCentral()
maven {
url "https://storage.googleapis.com/download.flutter.io"
}
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
namespace 'com.example.ai_chat_assistant'
compileSdk 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
}
defaultConfig {
minSdkVersion 21
ndk {
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
buildFeatures {
dataBinding true
viewBinding = true
}
// 获取配置文件
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') {
localProperties.load(it)
}
}
// 获取flutter sdk 路径
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.")
}
// 找到 flutter.jar 文件
def flutterJar = new File(flutterRoot, "bin/cache/artifacts/engine/android-arm/flutter.jar")
if (!flutterJar.exists()) {
// 尝试其他路径
flutterJar = new File(flutterRoot, "bin/cache/artifacts/engine/android-arm-release/flutter.jar")
}
if (!flutterJar.exists()) {
// Windows 上可能在这个位置
flutterJar = new File(flutterRoot, "bin/cache/artifacts/engine/android-x64/flutter.jar")
}
repositories {
flatDir {
dirs("libs")
}
}
dependencies {
implementation 'com.alibaba.idst:nui-release:1.0.0'
implementation 'com.alibaba:fastjson:1.2.83'
// 使用Flutter的官方Maven依赖
// compileOnly 'io.flutter:flutter_embedding_debug:1.0.0-3.27.4'
// 或者使用release版本
// compileOnly '"io.flutter:flutter_embedding_release:1.0.0-de555ebd6cfe3e606a101b9ae45bbf5e62d4e4e1"'
// compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar")
compileOnly files(files("libs/flutter.jar"))
// implementation(files("libs/fastjson-1.1.46.android.jar"))
// implementation(files("libs/nui-release-1.0.0.aar"))
}
}

File diff suppressed because one or more lines are too long

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.12-all.zip distributionUrl=https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.13-all.zip

BIN
android/libs/flutter.jar Normal file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba.idst</groupId>
<artifactId>nui-release</artifactId>
<version>1.0.0</version>
<packaging>aar</packaging>
</project>

1
android/settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'ai_assistant_plugin'

View File

@@ -1,25 +0,0 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

View File

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -1,32 +1,32 @@
package com.example.ai_chat_assistant; package com.example.ai_assistant_plugin;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.idst.nui.AsrResult;
import com.alibaba.idst.nui.Constants;
import com.alibaba.idst.nui.INativeNuiCallback;
import com.alibaba.idst.nui.INativeStreamInputTtsCallback;
import com.alibaba.idst.nui.KwsResult;
import com.alibaba.idst.nui.NativeNui;
import java.util.Map; import java.util.Map;
public class MainActivity extends FlutterActivity implements INativeNuiCallback { import io.flutter.embedding.engine.plugins.FlutterPlugin;
private static final String TTS_CHANNEL = "com.example.ai_chat_assistant/tts"; import io.flutter.plugin.common.MethodCall;
private static final String ASR_CHANNEL = "com.example.ai_chat_assistant/asr"; import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.idst.nui.Constants;
import com.alibaba.idst.nui.INativeStreamInputTtsCallback;
import com.alibaba.idst.nui.NativeNui;
/**
* AiAssistantPlugin
*/
public class AiAssistantPlugin implements FlutterPlugin, MethodCallHandler {
private static final String CHANNEL = "com.example.ai_chat_assistant/ali_sdk";
private static final String TAG = "AliyunSDK"; private static final String TAG = "AliyunSDK";
private static final String APP_KEY = "bXFFc1V65iYbW6EF"; private static final String APP_KEY = "bXFFc1V65iYbW6EF";
private static final String ACCESS_KEY = "LTAI5t71JHxXRvt2mGuEVz9X"; private static final String ACCESS_KEY = "LTAI5t71JHxXRvt2mGuEVz9X";
@@ -37,6 +37,15 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
private final NativeNui streamInputTtsInstance = new NativeNui(Constants.ModeType.MODE_STREAM_INPUT_TTS); private final NativeNui streamInputTtsInstance = new NativeNui(Constants.ModeType.MODE_STREAM_INPUT_TTS);
private final NativeNui asrInstance = new NativeNui(); private final NativeNui asrInstance = new NativeNui();
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
private Handler asrHandler;
private AsrCallBack asrCallBack;
private Handler handler = new Handler(Looper.getMainLooper());
private final AudioPlayer ttsAudioTrack = new AudioPlayer(new AudioPlayerCallback() { private final AudioPlayer ttsAudioTrack = new AudioPlayer(new AudioPlayerCallback() {
@Override @Override
public void playStart() { public void playStart() {
@@ -53,137 +62,66 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
} }
}); });
private MethodChannel asrMethodChannel;
private final static int ASR_SAMPLE_RATE = 16000;
private final static int ASR_WAVE_FRAM_SIZE = 20 * 2 * 1 * ASR_SAMPLE_RATE / 1000; //20ms audio for 16k/16bit/mono
private AudioRecord asrAudioRecorder = null;
private boolean asrStopping = false;
private Handler asrHandler;
private String asrText = "";
@Override @Override
public void configureFlutterEngine(FlutterEngine flutterEngine) { public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
super.configureFlutterEngine(flutterEngine); channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), TTS_CHANNEL) channel.setMethodCallHandler(this);
.setMethodCallHandler((call, result) -> {
Map<String, Object> args = (Map<String, Object>) call.arguments;
switch (call.method) {
case "startTts":
Object isChinese = args.get("isChinese");
if (isChinese == null || isChinese.toString().isBlank()) {
return;
}
boolean isSuccess = startTts(Boolean.parseBoolean(isChinese.toString()));
result.success(isSuccess);
break;
case "sendTts":
Object textArg = args.get("text");
if (textArg == null || textArg.toString().isBlank()) {
return;
}
sendTts(textArg.toString());
break;
case "completeTts":
completeTts();
break;
case "stopTts":
stopTts();
break;
default:
result.notImplemented();
break;
}
});
asrMethodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), ASR_CHANNEL);
asrMethodChannel.setMethodCallHandler((call, result) -> {
switch (call.method) {
case "startAsr":
startAsr();
break;
case "stopAsr":
stopAsr();
break;
default:
result.notImplemented();
break;
}
});
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
HandlerThread asrHandlerThread = new HandlerThread("process_thread"); HandlerThread asrHandlerThread = new HandlerThread("process_thread");
asrHandlerThread.start(); asrHandlerThread.start();
asrHandler = new Handler(asrHandlerThread.getLooper()); asrHandler = new Handler(asrHandlerThread.getLooper());
} asrCallBack = new AsrCallBack(channel);
asrInstance.initialize(asrCallBack, genAsrInitParams(),
@Override
protected void onStart() {
Log.i(TAG, "onStart");
super.onStart();
asrInstance.initialize(this, genAsrInitParams(),
Constants.LogLevel.LOG_LEVEL_NONE, false); Constants.LogLevel.LOG_LEVEL_NONE, false);
} }
@Override @Override
protected void onStop() { public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
Log.i(TAG, "onStop"); channel.setMethodCallHandler(null);
super.onStop();
asrInstance.release();
}
@Override
protected void onDestroy() {
super.onDestroy();
ttsAudioTrack.stop(); ttsAudioTrack.stop();
ttsAudioTrack.releaseAudioTrack(); ttsAudioTrack.releaseAudioTrack();
streamInputTtsInstance.stopStreamInputTts(); streamInputTtsInstance.stopStreamInputTts();
streamInputTtsInstance.release();
// asrInstance.release();
// if (asrCallBack != null) {
// asrCallBack.release();
// asrCallBack = null;
// }
} }
private boolean startTts(boolean isChinese) { @Override
int ret = streamInputTtsInstance.startStreamInputTts(new INativeStreamInputTtsCallback() { public void onMethodCall(MethodCall call, @NonNull Result result) {
@Override Map<String, Object> args = call.arguments();
public void onStreamInputTtsEventCallback(INativeStreamInputTtsCallback.StreamInputTtsEvent event, String task_id, String session_id, int ret_code, String error_msg, String timestamp, String all_response) { switch (call.method) {
Log.i(TAG, "stream input tts event(" + event + ") session id(" + session_id + ") task id(" + task_id + ") retCode(" + ret_code + ") errMsg(" + error_msg + ")"); case "startTts":
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED) { Object isChinese = args.get("isChinese");
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED"); if (isChinese == null || isChinese.toString().isBlank()) {
ttsAudioTrack.play(); return;
Log.i(TAG, "start play");
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS:" + timestamp);
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE || event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
/*
* 提示: STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE事件表示TTS已经合成完并通过回调传回了所有音频数据, 而不是表示播放器已经播放完了所有音频数据
*/
Log.i(TAG, "play end");
// 表示推送完数据, 当播放器播放结束则会有playOver回调
ttsAudioTrack.isFinishSend(true);
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
Log.e(TAG, "STREAM_INPUT_TTS_EVENT_TASK_FAILED: " + "error_code(" + ret_code + ") error_message(" + error_msg + ")");
}
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN:" + all_response);
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_END) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_END:" + all_response);
} }
} boolean isSuccess = startTts(Boolean.parseBoolean(isChinese.toString()));
result.success(isSuccess);
@Override break;
public void onStreamInputTtsDataCallback(byte[] data) { case "sendTts":
if (data.length > 0) { Object textArg = args.get("text");
ttsAudioTrack.setAudioData(data); if (textArg == null || textArg.toString().isBlank()) {
return;
} }
} sendTts(textArg.toString());
}, genTtsTicket(), genTtsParameters(isChinese), "", Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_NONE), false); break;
if (Constants.NuiResultCode.SUCCESS != ret) { case "completeTts":
Log.i(TAG, "start tts failed " + ret); completeTts();
return false; break;
} else { case "stopTts":
return true; stopTts();
break;
case "startAsr":
startAsr();
break;
case "stopAsr":
stopAsr();
break;
default:
result.notImplemented();
break;
} }
} }
@@ -201,7 +139,6 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
} }
private void startAsr() { private void startAsr() {
asrText = "";
asrHandler.post(() -> { asrHandler.post(() -> {
String setParamsString = genAsrParams(); String setParamsString = genAsrParams();
Log.i(TAG, "nui set params " + setParamsString); Log.i(TAG, "nui set params " + setParamsString);
@@ -213,105 +150,12 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
private void stopAsr() { private void stopAsr() {
asrHandler.post(() -> { asrHandler.post(() -> {
asrStopping = true;
long ret = asrInstance.stopDialog(); long ret = asrInstance.stopDialog();
runOnUiThread(() -> asrMethodChannel.invokeMethod("onAsrStop", null)); handler.post(() -> channel.invokeMethod("onAsrStop", null));
Log.i(TAG, "cancel dialog " + ret + " end"); Log.i(TAG, "cancel dialog " + ret + " end");
}); });
} }
@Override
public void onNuiEventCallback(Constants.NuiEvent event, final int resultCode,
final int arg2, KwsResult kwsResult,
AsrResult asrResult) {
Log.i(TAG, "event=" + event + " resultCode=" + resultCode);
if (event == Constants.NuiEvent.EVENT_TRANSCRIBER_STARTED) {
} else if (event == Constants.NuiEvent.EVENT_TRANSCRIBER_COMPLETE) {
asrStopping = false;
} else if (event == Constants.NuiEvent.EVENT_ASR_PARTIAL_RESULT) {
JSONObject jsonObject = JSON.parseObject(asrResult.allResponse);
JSONObject payload = jsonObject.getJSONObject("payload");
String result = payload.getString("result");
if (asrMethodChannel != null && result != null && !result.isBlank()) {
runOnUiThread(() -> asrMethodChannel.invokeMethod("onAsrResult", asrText + result));
}
} else if (event == Constants.NuiEvent.EVENT_SENTENCE_END) {
JSONObject jsonObject = JSON.parseObject(asrResult.allResponse);
JSONObject payload = jsonObject.getJSONObject("payload");
String result = payload.getString("result");
if (asrMethodChannel != null && result != null && !result.isBlank()) {
asrText += result;
runOnUiThread(() -> asrMethodChannel.invokeMethod("onAsrResult", asrText));
}
} else if (event == Constants.NuiEvent.EVENT_VAD_START) {
} else if (event == Constants.NuiEvent.EVENT_VAD_END) {
} else if (event == Constants.NuiEvent.EVENT_ASR_ERROR) {
asrStopping = false;
} else if (event == Constants.NuiEvent.EVENT_MIC_ERROR) {
asrStopping = false;
} else if (event == Constants.NuiEvent.EVENT_DIALOG_EX) { /* unused */
Log.i(TAG, "dialog extra message = " + asrResult.asrResult);
}
}
//当调用NativeNui的start后会一定时间反复回调该接口底层会提供buffer并告知这次需要数据的长度
//返回值告知底层读了多少数据应该尽量保证return的长度等于需要的长度如果返回<=0则表示出错
@Override
public int onNuiNeedAudioData(byte[] buffer, int len) {
if (asrAudioRecorder == null) {
return -1;
}
if (asrAudioRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "audio recorder not init");
return -1;
}
return asrAudioRecorder.read(buffer, 0, len);
}
//当录音状态发送变化的时候调用
@Override
public void onNuiAudioStateChanged(Constants.AudioState state) {
Log.i(TAG, "onNuiAudioStateChanged");
if (state == Constants.AudioState.STATE_OPEN) {
Log.i(TAG, "audio recorder start");
asrAudioRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
ASR_SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
ASR_WAVE_FRAM_SIZE * 4);
asrAudioRecorder.startRecording();
Log.i(TAG, "audio recorder start done");
} else if (state == Constants.AudioState.STATE_CLOSE) {
Log.i(TAG, "audio recorder close");
if (asrAudioRecorder != null) {
asrAudioRecorder.release();
}
} else if (state == Constants.AudioState.STATE_PAUSE) {
Log.i(TAG, "audio recorder pause");
if (asrAudioRecorder != null) {
asrAudioRecorder.stop();
}
}
}
@Override
public void onNuiAudioRMSChanged(float val) {
// Log.i(TAG, "onNuiAudioRMSChanged vol " + val);
}
@Override
public void onNuiVprEventCallback(Constants.NuiVprEvent event) {
Log.i(TAG, "onNuiVprEventCallback event " + event);
}
@Override
public void onNuiLogTrackCallback(Constants.LogLevel level, String log) {
Log.i(TAG, "onNuiLogTrackCallback log level:" + level + ", message -> " + log);
}
private String genTtsTicket() { private String genTtsTicket() {
String str = ""; String str = "";
try { try {
@@ -433,4 +277,51 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
Log.i(TAG, "dialog params: " + params); Log.i(TAG, "dialog params: " + params);
return params; return params;
} }
}
private boolean startTts(boolean isChinese) {
int ret = streamInputTtsInstance.startStreamInputTts(new INativeStreamInputTtsCallback() {
@Override
public void onStreamInputTtsEventCallback(INativeStreamInputTtsCallback.StreamInputTtsEvent event, String task_id, String session_id, int ret_code, String error_msg, String timestamp, String all_response) {
Log.i(TAG, "stream input tts event(" + event + ") session id(" + session_id + ") task id(" + task_id + ") retCode(" + ret_code + ") errMsg(" + error_msg + ")");
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SYNTHESIS_STARTED");
ttsAudioTrack.play();
Log.i(TAG, "start play");
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_SYNTHESIS:" + timestamp);
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE || event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
/*
* 提示: STREAM_INPUT_TTS_EVENT_SYNTHESIS_COMPLETE事件表示TTS已经合成完并通过回调传回了所有音频数据, 而不是表示播放器已经播放完了所有音频数据
*/
Log.i(TAG, "play end");
// 表示推送完数据, 当播放器播放结束则会有playOver回调
ttsAudioTrack.isFinishSend(true);
if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_TASK_FAILED) {
Log.e(TAG, "STREAM_INPUT_TTS_EVENT_TASK_FAILED: " + "error_code(" + ret_code + ") error_message(" + error_msg + ")");
}
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_BEGIN:" + all_response);
} else if (event == StreamInputTtsEvent.STREAM_INPUT_TTS_EVENT_SENTENCE_END) {
Log.i(TAG, "STREAM_INPUT_TTS_EVENT_SENTENCE_END:" + all_response);
}
}
@Override
public void onStreamInputTtsDataCallback(byte[] data) {
if (data.length > 0) {
ttsAudioTrack.setAudioData(data);
}
}
}, genTtsTicket(), genTtsParameters(isChinese), "", Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_NONE), false);
if (Constants.NuiResultCode.SUCCESS != ret) {
Log.i(TAG, "start tts failed " + ret);
return false;
} else {
return true;
}
}
}

View File

@@ -0,0 +1,139 @@
package com.example.ai_assistant_plugin;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.idst.nui.AsrResult;
import com.alibaba.idst.nui.Constants;
import com.alibaba.idst.nui.INativeNuiCallback;
import com.alibaba.idst.nui.KwsResult;
import io.flutter.plugin.common.MethodChannel;
public class AsrCallBack implements INativeNuiCallback {
private static final String TAG = "AliAsr";
private final static int ASR_SAMPLE_RATE = 16000;
private final static int ASR_WAVE_FRAM_SIZE = 20 * 2 * 1 * ASR_SAMPLE_RATE / 1000; //20ms audio for 16k/16bit/mono
private AudioRecord asrAudioRecorder = null;
private MethodChannel channel;
private String asrText = "";
private Handler handler = new Handler(Looper.getMainLooper());
public AsrCallBack(MethodChannel channel) {
this.channel = channel;
}
@Override
public void onNuiEventCallback(Constants.NuiEvent event, final int resultCode,
final int arg2, KwsResult kwsResult,
AsrResult asrResult) {
Log.i(TAG, "event=" + event + " resultCode=" + resultCode);
if (event == Constants.NuiEvent.EVENT_TRANSCRIBER_STARTED) {
asrText = "";
} else if (event == Constants.NuiEvent.EVENT_TRANSCRIBER_COMPLETE) {
} else if (event == Constants.NuiEvent.EVENT_ASR_PARTIAL_RESULT) {
JSONObject jsonObject = JSON.parseObject(asrResult.allResponse);
JSONObject payload = jsonObject.getJSONObject("payload");
String result = payload.getString("result");
if (channel != null && result != null && !result.isBlank()) {
handler.post(() -> channel.invokeMethod("onAsrResult", asrText + result));
}
} else if (event == Constants.NuiEvent.EVENT_SENTENCE_END) {
JSONObject jsonObject = JSON.parseObject(asrResult.allResponse);
JSONObject payload = jsonObject.getJSONObject("payload");
String result = payload.getString("result");
if (channel != null && result != null && !result.isBlank()) {
asrText += result;
handler.post(() -> channel.invokeMethod("onAsrResult", asrText));
}
} else if (event == Constants.NuiEvent.EVENT_VAD_START) {
} else if (event == Constants.NuiEvent.EVENT_VAD_END) {
} else if (event == Constants.NuiEvent.EVENT_ASR_ERROR) {
} else if (event == Constants.NuiEvent.EVENT_MIC_ERROR) {
} else if (event == Constants.NuiEvent.EVENT_DIALOG_EX) { /* unused */
Log.i(TAG, "dialog extra message = " + asrResult.asrResult);
}
}
//当调用NativeNui的start后会一定时间反复回调该接口底层会提供buffer并告知这次需要数据的长度
//返回值告知底层读了多少数据应该尽量保证return的长度等于需要的长度如果返回<=0则表示出错
@Override
public int onNuiNeedAudioData(byte[] buffer, int len) {
if (asrAudioRecorder == null) {
return -1;
}
if (asrAudioRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "audio recorder not init");
return -1;
}
return asrAudioRecorder.read(buffer, 0, len);
}
//当录音状态发送变化的时候调用
@Override
public void onNuiAudioStateChanged(Constants.AudioState state) {
Log.i(TAG, "onNuiAudioStateChanged");
if (state == Constants.AudioState.STATE_OPEN) {
Log.i(TAG, "audio recorder start");
asrAudioRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
ASR_SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
ASR_WAVE_FRAM_SIZE * 4);
asrAudioRecorder.startRecording();
Log.i(TAG, "audio recorder start done");
} else if (state == Constants.AudioState.STATE_CLOSE) {
Log.i(TAG, "audio recorder close");
if (asrAudioRecorder != null) {
asrAudioRecorder.release();
}
} else if (state == Constants.AudioState.STATE_PAUSE) {
Log.i(TAG, "audio recorder pause");
if (asrAudioRecorder != null) {
asrAudioRecorder.stop();
}
}
}
@Override
public void onNuiAudioRMSChanged(float val) {
// Log.i(TAG, "onNuiAudioRMSChanged vol " + val);
}
@Override
public void onNuiVprEventCallback(Constants.NuiVprEvent event) {
Log.i(TAG, "onNuiVprEventCallback event " + event);
}
@Override
public void onNuiLogTrackCallback(Constants.LogLevel level, String log) {
Log.i(TAG, "onNuiLogTrackCallback log level:" + level + ", message -> " + log);
}
public void release() {
// 释放音频录制资源
if (asrAudioRecorder != null) {
try {
asrAudioRecorder.stop();
} catch (Exception e) {
Log.e(TAG, "release error", e);
}
asrAudioRecorder.release();
asrAudioRecorder = null;
}
channel = null;
asrText = null;
}
}

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant; package com.example.ai_assistant_plugin;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant; package com.example.ai_assistant_plugin;
public interface AudioPlayerCallback { public interface AudioPlayerCallback {
public void playStart(); public void playStart();

View File

@@ -1,11 +1,10 @@
package com.example.ai_chat_assistant; package com.example.ai_assistant_plugin;
import android.util.Log; import android.util.Log;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.example.ai_assistant_plugin.token.AccessToken;
import com.example.ai_chat_assistant.token.AccessToken;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token; package com.example.ai_assistant_plugin.token;
import android.util.Log; import android.util.Log;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token; package com.example.ai_assistant_plugin.token;
import android.util.Log; import android.util.Log;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token; package com.example.ai_assistant_plugin.token;
/** /**
* Say something * Say something

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token; package com.example.ai_assistant_plugin.token;
import android.util.Log; import android.util.Log;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token; package com.example.ai_assistant_plugin.token;
import android.util.Base64; import android.util.Base64;

BIN
assets/images/ai1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
assets/images/ai2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

45
example/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
example/.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "05db9689081f091050f01aed79f04dce0c750154"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
- platform: android
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
example/README.md Normal file
View File

@@ -0,0 +1,16 @@
# example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
example/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -6,9 +6,9 @@ plugins {
} }
android { android {
namespace = "com.example.ai_chat_assistant" namespace = "com.example.example"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = "29.0.13599879" ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
@@ -21,10 +21,10 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.ai_chat_assistant" applicationId = "com.example.example"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 24 minSdk = 23
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
@@ -37,11 +37,14 @@ android {
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
} }
} }
repositories { viewBinding {
flatDir { // isEnabled = viewBindingEnabled
dirs("libs")
}
} }
dependenciesInfo {
includeInApk = includeInApk
includeInBundle = includeInBundle
}
ndkVersion = "27.0.12077973"
} }
flutter { flutter {
@@ -49,6 +52,9 @@ flutter {
} }
dependencies { dependencies {
implementation(files("libs/fastjson-1.1.46.android.jar")) // Process .aar files in the libs folder.
implementation(files("libs/nuisdk-release.aar")) val aarFiles = fileTree(mapOf("dir" to "$rootDir/libs", "include" to listOf("*.aar")))
aarFiles.forEach { aar ->
implementation(files(aar))
}
} }

View File

@@ -4,5 +4,4 @@
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.intent.action.TTS_SERVICE" />
</manifest> </manifest>

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application <application
android:label="ai_chat_assistant" android:label="AI助手"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
@@ -41,8 +41,5 @@
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
<intent>
<action android:name="android.intent.action.TTS_SERVICE"/>
</intent>
</queries> </queries>
</manifest> </manifest>

View File

@@ -0,0 +1,6 @@
package com.example.example;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}

View File

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 544 B

View File

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

View File

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 721 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -1,16 +1,25 @@
allprojects { allprojects {
repositories { repositories {
val mavenLocalPath = rootProject.file("../../android/mavenLocal")
// println("Maven local path: ${mavenLocalPath.absolutePath}")
// println("Maven local exists: ${mavenLocalPath.exists()}")
maven { url = uri(mavenLocalPath) }
maven { url = uri("https://maven.aliyun.com/repository/public") }
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
maven { url = uri("https://maven.aliyun.com/repository/central") }
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://maven.aliyun.com/repository/public/") }
maven { url = uri("https://maven.aliyun.com/repository/google/") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin/") }
maven { url = uri("https://storage.googleapis.com/download.flutter.io")}
} }
} }
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir) rootProject.layout.buildDirectory.value(newBuildDir)
subprojects { subprojects {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

Binary file not shown.

View File

@@ -0,0 +1,33 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
maven { url = uri("https://maven.aliyun.com/repository/public") }
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
maven { url = uri("https://maven.aliyun.com/repository/central") }
google()
mavenCentral()
gradlePluginPortal()
flatDir { dirs(file("libs")) }
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

94
example/lib/main.dart Normal file
View File

@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
class VehicleCommandClent extends VehicleCommandHandler {
@override
AIChatCommandCubit? get commandCubit => super.commandCubit;
@override
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command) async {
// 在这里实现具体的车控命令执行逻辑
print('执行车控命令: ${command.type}, 参数: ${command.params}');
if (commandCubit != null) {
commandCubit?.emit(AIChatCommandState(
commandId: command.commandId,
commandType: command.type,
params: command.params,
status: AIChatCommandStatus.executing,
timestamp: DateTime.now(),
errorMessage: null,
result: null,
));
}
// 模拟命令执行完成
await Future.delayed(const Duration(seconds: 2));
if (commandCubit != null) {
commandCubit?.emit(AIChatCommandState(
commandId: command.commandId,
commandType: command.type,
params: command.params,
status: AIChatCommandStatus.success,
timestamp: DateTime.now(),
errorMessage: null,
result: {'message': '命令执行成功'},
));
}
return Future.value((true, {'message': '命令已执行'}));
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 请求麦克风权限
if (!await Permission.microphone.isGranted) {
await Permission.microphone.request();
}
// 初始化 AI Chat Assistant注册车控命令回调
// ChatAssistantApp.initialize(
// commandCallback: (VehicleCommandType type, Map<String, dynamic>? params) async {
// // 这里是示例的车控命令处理逻辑
// print('收到车控命令: $type, 参数: $params');
// return Future.value((true, {'message': '命令已执行'}));
// },
// );
AIChatAssistantManager.instance.setupCommandHandle(commandHandler: VehicleCommandClent());
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AI Chat Assistant Example',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const ExampleHomePage(),
);
}
}
class ExampleHomePage extends StatelessWidget {
const ExampleHomePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: ChatAssistantApp(),
);
}
}

608
example/pubspec.lock Normal file
View File

@@ -0,0 +1,608 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
ai_chat_assistant:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "1.0.0+1"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
audioplayers:
dependency: transitive
description:
name: audioplayers
sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.2.1"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.3"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.2"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
basic_intl:
dependency: transitive
description:
path: "../packages/basic_intl"
relative: true
source: path
version: "0.2.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.6"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
flutter_markdown:
dependency: transitive
description:
name: flutter_markdown
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.7+1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_tts:
dependency: transitive
description:
name: flutter_tts
sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.3"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
fluttertoast:
dependency: transitive
description:
name: fluttertoast
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.2.12"
http:
dependency: transitive
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.7"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.1"
markdown:
dependency: transitive
description:
name: markdown
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.15.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
permission_handler:
dependency: transitive
description:
name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.flutter-io.cn"
source: hosted
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.5+1"
record:
dependency: transitive
description:
name: record
sha256: "9dbc6ff3e784612f90a9b001373c45ff76b7a08abd2bd9fdf72c242320c8911c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.1"
record_android:
dependency: transitive
description:
name: record_android
sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
record_ios:
dependency: transitive
description:
name: record_ios
sha256: "13e241ed9cbc220534a40ae6b66222e21288db364d96dd66fb762ebd3cb77c71"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: "235b1f1fb84e810f8149cc0c2c731d7d697f8d1c333b32cb820c449bf7bb72d8"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
record_macos:
dependency: transitive
description:
name: record_macos
sha256: "2849068bb59072f300ad63ed146e543d66afaef8263edba4de4834fc7c8d4d35"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
sha256: b0065fdf1ec28f5a634d676724d388a77e43ce7646fb049949f58c69f3fcb4ed
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
record_web:
dependency: transitive
description:
name: record_web
sha256: "4f0adf20c9ccafcc02d71111fd91fba1ca7b17a7453902593e5a9b25b74a5c56"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
record_windows:
dependency: transitive
description:
name: record_windows
sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.7"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.0+3"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.3"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.7"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.flutter-io.cn"
source: hosted
version: "14.3.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0"

93
example/pubspec.yaml Normal file
View File

@@ -0,0 +1,93 @@
name: ai_chat_example
description: "AI Chat Assistant Example App"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.5.0
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# 添加本地 AI Chat Assistant plugin 依赖
ai_chat_assistant:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

View File

@@ -0,0 +1,29 @@
// widgets
export 'pages/full_screen.dart';
export 'screens/part_screen.dart';
export 'app.dart';
export 'widgets/floating_icon.dart';
// easy bloc
export 'bloc/easy_bloc.dart';
export 'bloc/ai_chat_cubit.dart';
export 'bloc/command_state.dart';
// services
export 'manager.dart';
export 'services/message_service.dart';
export 'services/command_service.dart';
// types && enums && models
export 'models/vehicle_cmd.dart';
export 'models/vehicle_cmd_response.dart';
export 'models/chat_message.dart';
export 'models/vehicle_status_info.dart';
export 'enums/vehicle_command_type.dart';
export 'enums/message_status.dart';
export 'enums/message_service_state.dart';
// utils
export 'utils/assets_util.dart';

View File

@@ -0,0 +1,24 @@
import '../enums/vehicle_command_type.dart';
import '../services/command_service.dart';
import 'easy_bloc.dart';
import 'command_state.dart';
class AIChatCommandCubit extends EasyCubit<AIChatCommandState> {
AIChatCommandCubit() : super(const AIChatCommandState());
// 重置状态
void reset() {
emit(const AIChatCommandState());
}
// 生成唯一命令ID
String _generateCommandId() {
return '${DateTime.now().millisecondsSinceEpoch}_${state.commandType?.name ?? 'unknown'}';
}
// 检查当前是否有命令在执行
bool get isExecuting => state.status == AIChatCommandStatus.executing;
// 获取当前命令ID
String? get currentCommandId => state.commandId;
}

View File

@@ -0,0 +1,68 @@
import '../enums/vehicle_command_type.dart';
// AI Chat Command States
enum AIChatCommandStatus {
idle,
executing,
success,
failure,
cancelled,
}
class AIChatCommandState {
final String? commandId;
final VehicleCommandType? commandType;
final Map<String, dynamic>? params;
final AIChatCommandStatus status;
final String? errorMessage;
final Map<String, dynamic>? result;
final DateTime? timestamp;
const AIChatCommandState({
this.commandId,
this.commandType,
this.params,
this.status = AIChatCommandStatus.idle,
this.errorMessage,
this.result,
this.timestamp,
});
AIChatCommandState copyWith({
String? commandId,
VehicleCommandType? commandType,
Map<String, dynamic>? params,
AIChatCommandStatus? status,
String? errorMessage,
Map<String, dynamic>? result,
DateTime? timestamp,
}) {
return AIChatCommandState(
commandId: commandId ?? this.commandId,
commandType: commandType ?? this.commandType,
params: params ?? this.params,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
result: result ?? this.result,
timestamp: timestamp ?? this.timestamp,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AIChatCommandState &&
other.commandId == commandId &&
other.commandType == commandType &&
other.status == status &&
other.errorMessage == errorMessage;
}
@override
int get hashCode {
return commandId.hashCode ^
commandType.hashCode ^
status.hashCode ^
errorMessage.hashCode;
}
}

78
lib/bloc/easy_bloc.dart Normal file
View File

@@ -0,0 +1,78 @@
import 'dart:async';
import 'package:meta/meta.dart';
// 简易的Bloc, 简易的状态管理
abstract class EasyBlocBase<State> {
EasyBlocBase(this.state): assert(state != null);
final StreamController<State> _stateController = StreamController<State>.broadcast();
Stream<State> get stream => _stateController.stream;
State state;
bool _emitted = false;
// 添加监听器方法
StreamSubscription<State> listen(
void Function(State state) onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
return stream.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
// 新状态
void emit(State newState) {
if (_stateController.isClosed) return;
if (newState == state && _emitted) return;
this.state = newState;
_stateController.add(state);
_emitted = true;
}
@mustCallSuper
Future<void> close() async {
await _stateController.close();
}
}
class EasyBloc<Event, State> extends EasyBlocBase<State> {
EasyBloc(State initialState) : super(initialState) {
_eventController.stream.listen(_mapEventToState);
}
final StreamController<Event> _eventController = StreamController<Event>();
// 子类需要实现这个方法来处理事件
@mustBeOverridden
void _mapEventToState(Event event) {
// 默认不做任何处理,如果不实现会抛出异常
throw UnimplementedError('_mapEventToState must be implemented by subclasses');
}
// 添加事件
void add(Event event) {
if (_eventController.isClosed) return;
_eventController.add(event);
}
@override
Future<void> close() async {
await _eventController.close();
await super.close();
}
}
class EasyCubit<State> extends EasyBlocBase<State> {
EasyCubit(State initialState) : super(initialState);
}

View File

@@ -3,4 +3,4 @@ enum MessageServiceState {
recording, recording,
recognizing, recognizing,
replying, replying,
} }

View File

@@ -1,18 +0,0 @@
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'app.dart';
import 'package:provider/provider.dart';
import 'services/message_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (!await Permission.microphone.isGranted) {
await Permission.microphone.request();
}
runApp(
ChangeNotifierProvider(
create: (_) => MessageService(),
child: const ChatAssistantApp(),
),
);
}

52
lib/manager.dart Normal file
View File

@@ -0,0 +1,52 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'bloc/ai_chat_cubit.dart';
import 'bloc/command_state.dart';
import 'models/vehicle_cmd.dart';
/// 车辆命令处理器抽象类
abstract class VehicleCommandHandler {
AIChatCommandCubit? commandCubit;
/// 执行车辆控制命令
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command);
}
class AIChatAssistantManager {
static final AIChatAssistantManager _instance = AIChatAssistantManager._internal();
static AIChatAssistantManager get instance => _instance;
AIChatAssistantManager._internal() {
// 初始化代码
debugPrint('AIChatAssistant 单例创建');
_commandCubit = AIChatCommandCubit();
}
AIChatCommandCubit? _commandCubit;
VehicleCommandHandler? commandClient;
/// 初始化命令处理器
void setupCommandHandle({
required VehicleCommandHandler commandHandler,
}) {
commandClient = commandHandler;
commandClient?.commandCubit = _commandCubit;
}
/// 获取命令状态流
Stream<AIChatCommandState> get commandStateStream {
if (_commandCubit == null) {
throw StateError('AIChatAssistant 未初始化');
}
return _commandCubit!.stream;
}
/// 释放资源
void dispose() {
_commandCubit?.close();
_commandCubit = null;
}
}

View File

@@ -1,11 +1,15 @@
import '../enums/vehicle_command_type.dart'; import '../enums/vehicle_command_type.dart';
/// 车辆控制命令类
class VehicleCommand { class VehicleCommand {
final VehicleCommandType type; final VehicleCommandType type;
final Map<String, dynamic>? params; final Map<String, dynamic>? params;
final String error; final String error;
late String commandId;
VehicleCommand({required this.type, this.params, this.error = ''}); VehicleCommand({required this.type, this.params, this.error = ''}) {
commandId = DateTime.now().millisecondsSinceEpoch.toString();
}
// 从字符串创建命令用于从API响应解析 // 从字符串创建命令用于从API响应解析
factory VehicleCommand.fromString( factory VehicleCommand.fromString(

View File

@@ -9,14 +9,14 @@ import 'package:provider/provider.dart';
import '../widgets/chat_header.dart'; import '../widgets/chat_header.dart';
import '../widgets/chat_footer.dart'; import '../widgets/chat_footer.dart';
class FullScreen extends StatefulWidget { class FullScreenPage extends StatefulWidget {
const FullScreen({super.key}); const FullScreenPage({super.key});
@override @override
State<FullScreen> createState() => _FullScreenState(); State<FullScreenPage> createState() => _FullScreenState();
} }
class _FullScreenState extends State<FullScreen> { class _FullScreenState extends State<FullScreenPage> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
@override @override

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widgets/floating_icon.dart'; import '../widgets/floating_icon.dart';
import '../utils/assets_util.dart';
import '../widgets/chat_popup.dart';
import '../widgets/chat_floating_icon.dart';
class MainScreen extends StatefulWidget { class MainScreen extends StatefulWidget {
const MainScreen({super.key}); const MainScreen({super.key});
@@ -26,14 +29,16 @@ class _MainScreenState extends State<MainScreen> {
body: Stack( body: Stack(
children: [ children: [
Container( Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: AssetImage('assets/images/bg.jpg'), image: AssetsUtil.getImage('bg.jpg'),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
), ),
FloatingIcon(), // FloatingIcon(),
ChatPopup(
)
], ],
), ),
); );

View File

@@ -1,15 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:ui'; import 'dart:ui';
import '../widgets/chat_box.dart'; import '../widgets/chat_box.dart';
import '../screens/full_screen.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../services/message_service.dart'; import '../services/message_service.dart';
import '../widgets/gradient_background.dart'; import '../widgets/gradient_background.dart';
import '../utils/assets_util.dart';
import '../pages/full_screen.dart';
class PartScreen extends StatefulWidget { class PartScreen extends StatefulWidget {
final VoidCallback? onHide; final VoidCallback? onHide;
final Offset floatingIconPosition;
final double iconSize;
const PartScreen({super.key, this.onHide}); const PartScreen({
super.key,
this.onHide,
required this.floatingIconPosition,
required this.iconSize,
});
@override @override
State<PartScreen> createState() => _PartScreenState(); State<PartScreen> createState() => _PartScreenState();
@@ -50,14 +58,80 @@ class _PartScreenState extends State<PartScreen> {
} }
void _openFullScreen() async { void _openFullScreen() async {
final messageService = context.read<MessageService>();
widget.onHide?.call(); widget.onHide?.call();
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const FullScreen(), builder: (context) => ChangeNotifierProvider.value(
value: messageService, // 传递同一个单例实例
child: const FullScreenPage(),
),
), ),
); );
} }
// 计算PartScreen的位置使其显示在FloatingIcon附近
EdgeInsets _calculatePosition(BoxConstraints constraints) {
final screenWidth = constraints.maxWidth;
final screenHeight = constraints.maxHeight;
// 聊天框的尺寸
final chatWidth = screenWidth * 0.94;
final maxChatHeight = screenHeight * 0.4;
final iconX = screenWidth - widget.floatingIconPosition.dx;
final iconY = screenHeight - widget.floatingIconPosition.dy;
// 1. 先将icon的right/bottom转为屏幕坐标icon左下角
var iconPosition = widget.floatingIconPosition;
final iconLeft = screenWidth - iconPosition.dx - widget.iconSize;
final iconBottom = iconPosition.dy;
final iconTop = iconBottom + widget.iconSize;
final iconCenterX = iconLeft + widget.iconSize / 2;
// 判断FloatingIcon在屏幕的哪一边
final isOnRightSide = iconX < screenWidth / 2;
// 计算水平位置
double leftPadding;
if (isOnRightSide) {
// 图标在右边,聊天框显示在左边
leftPadding = (screenWidth - chatWidth) * 0.1;
} else {
// 图标在左边,聊天框显示在右边
leftPadding = screenWidth - chatWidth - (screenWidth - chatWidth) * 0.1;
}
// 优先尝试放在icon上方
double chatBottom = iconTop + 12; // 聊天框底部距离icon顶部12px
double chatTop = screenHeight - chatBottom - maxChatHeight;
// 如果上方空间不足则放在icon下方
if (chatTop < 0) {
chatBottom = iconBottom - 12 - maxChatHeight / 2; // 聊天框顶部距离icon底部12px
// 如果下方也不足,则贴底
if (chatBottom < 0) {
chatBottom = 20; // 距离底部20px
}
}
// // 计算垂直位置,确保聊天框在图标上方
// double bottomPadding = iconY + widget.iconSize + 20;
//
// // 确保聊天框不会超出屏幕
// final minBottomPadding = screenHeight * 0.15;
// final maxBottomPadding = screenHeight - maxChatHeight - 50;
// bottomPadding = bottomPadding.clamp(minBottomPadding, maxBottomPadding);
return EdgeInsets.only(
left: leftPadding,
right: screenWidth - leftPadding - chatWidth,
bottom: chatBottom,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -67,9 +141,12 @@ class _PartScreenState extends State<PartScreen> {
if (!_isInitialized) { if (!_isInitialized) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final position = _calculatePosition(constraints);
final double minHeight = constraints.maxHeight * 0.16; final double minHeight = constraints.maxHeight * 0.16;
final double maxHeight = constraints.maxHeight * 0.4; final double maxHeight = constraints.maxHeight * 0.4;
final double chatWidth = constraints.maxWidth * 0.94; final double chatWidth = constraints.maxWidth * 0.94;
return Stack( return Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
@@ -92,74 +169,80 @@ class _PartScreenState extends State<PartScreen> {
), ),
), ),
), ),
Padding( Positioned(
padding: EdgeInsets.only(bottom: constraints.maxHeight * 0.22), left: position.left,
child: Align( right: position.right,
alignment: Alignment.bottomCenter, bottom: position.bottom,
child: Consumer<MessageService>( child: Consumer<MessageService>(
builder: (context, messageService, child) { builder: (context, messageService, child) {
final messageCount = messageService.messages.length; final messageCount = messageService.messages.length;
if (messageCount > _lastMessageCount) { if (messageCount > _lastMessageCount) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom(); _scrollToBottom();
}); });
} }
_lastMessageCount = messageCount; _lastMessageCount = messageCount;
return Container(
width: chatWidth, return TweenAnimationBuilder<double>(
constraints: BoxConstraints( tween: Tween<double>(begin: 0.8, end: 1.0),
minHeight: minHeight, duration: const Duration(milliseconds: 200),
maxHeight: maxHeight, curve: Curves.easeOutBack,
), builder: (context, scale, child) {
child: ClipRRect( return Transform.scale(
borderRadius: BorderRadius.circular(24), scale: scale,
child: BackdropFilter( child: Container(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), width: chatWidth,
child: GradientBackground( constraints: BoxConstraints(
colors: const [ minHeight: minHeight,
Color(0XBF3B0A3F), maxHeight: maxHeight,
Color(0xBF0E0E24), ),
Color(0xBF0C0B33), child: ClipRRect(
], borderRadius: BorderRadius.circular(24),
borderRadius: BorderRadius.circular(6), child: BackdropFilter(
child: Stack( filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
children: [ child: GradientBackground(
Padding( colors: const [
padding: const EdgeInsets.only( Color(0XBF3B0A3F),
top: 50, Color(0xBF0E0E24),
left: 6, Color(0xBF0C0B33),
right: 6, ],
bottom: 30), borderRadius: BorderRadius.circular(6),
child: LayoutBuilder( child: Stack(
builder: (context, boxConstraints) { children: [
return ChatBox( Padding(
scrollController: _scrollController, padding: const EdgeInsets.only(
messages: messageService.messages, top: 50, left: 6, right: 6, bottom: 30),
); child: LayoutBuilder(
} builder: (context, boxConstraints) {
), return ChatBox(
), scrollController: _scrollController,
Positioned( messages: messageService.messages,
top: 6, );
right: 6, }),
child: IconButton(
icon: Image.asset(
'assets/images/open_in_full.png',
width: 24,
height: 24
), ),
onPressed: _openFullScreen, Positioned(
padding: EdgeInsets.zero, top: 6,
), right: 6,
child: IconButton(
icon: AssetsUtil.getImageWidget(
'open_in_full.png',
width: 24,
height: 24,
),
onPressed: _openFullScreen,
padding: EdgeInsets.zero,
),
),
],
), ),
], ),
), ),
), ),
), ),
), );
); },
}, );
), },
), ),
), ),
], ],

View File

@@ -59,7 +59,8 @@ class ChatSseService {
txt = txt.substring(0, imgStart) + txt.substring(imgEnd + 1); txt = txt.substring(0, imgStart) + txt.substring(imgEnd + 1);
imgStart = txt.indexOf('!['); imgStart = txt.indexOf('![');
} }
// 彻底移除 markdown 有序/无序列表序号(如 1.、2.、-、*、+
txt = txt.replaceAll(RegExp(r'(^|\n)[ \t]*[0-9]+\.[ \t]*'), '\n');
txt = txt.replaceAll(RegExp(r'(^|\n)[ \t]*[-\*\+][ \t]+'), '\n'); txt = txt.replaceAll(RegExp(r'(^|\n)[ \t]*[-\*\+][ \t]+'), '\n');
// 分句符 // 分句符
RegExp enders = isChinese ? zhEnders : enEnders; RegExp enders = isChinese ? zhEnders : enEnders;
@@ -76,14 +77,8 @@ class ChatSseService {
} }
// 只在达到完整句子时调用 TtsUtil.send // 只在达到完整句子时调用 TtsUtil.send
for (final s in sentences) { for (final s in sentences) {
String ttsStr=CommonUtil.cleanText(s, true)+"\n"; String ttsStr=CommonUtil.cleanText(s, true);
// print("发送数据到TTS: $ttsStr");
ttsStr = ttsStr.replaceAllMapped(
RegExp(r'(?<!\s)(\d+)(?!\s)'),
(m) => ' ${m.group(1)} ',
);
print("发送数据到TTS: $ttsStr");
TtsUtil.send(ttsStr); TtsUtil.send(ttsStr);
} }
// 缓存剩余不完整部分 // 缓存剩余不完整部分

View File

@@ -5,7 +5,7 @@ import '../models/vehicle_status_info.dart';
typedef CommandCallback = Future<(bool, Map<String, dynamic>? params)> Function( typedef CommandCallback = Future<(bool, Map<String, dynamic>? params)> Function(
VehicleCommandType type, Map<String, dynamic>? params); VehicleCommandType type, Map<String, dynamic>? params);
/// 命令处理器类 - 负责处理来自AI的车辆控制命令 /// 命令处理器类 - 负责处理来自AI的车辆控制命令(使用回调的方式交给App去实现具体的车控逻辑)
class CommandService { class CommandService {
/// 保存主应用注册的回调函数 /// 保存主应用注册的回调函数
static CommandCallback? onCommandReceived; static CommandCallback? onCommandReceived;

View File

@@ -4,6 +4,7 @@ import '../models/vehicle_cmd.dart';
import '../models/vehicle_cmd_response.dart'; import '../models/vehicle_cmd_response.dart';
import 'vehicle_state_service.dart'; import 'vehicle_state_service.dart';
/// 车辆命令服务 - 负责与后端交互以获取和处理车辆控制命令
class VehicleCommandService { class VehicleCommandService {
// final VehicleStateService vehicleStateService = VehicleStateService(); // final VehicleStateService vehicleStateService = VehicleStateService();

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:ai_chat_assistant/ai_chat_assistant.dart';
import 'package:ai_chat_assistant/utils/common_util.dart'; import 'package:ai_chat_assistant/utils/common_util.dart';
import 'package:ai_chat_assistant/utils/tts_util.dart'; import 'package:ai_chat_assistant/utils/tts_util.dart';
import 'package:basic_intl/intl.dart'; import 'package:basic_intl/intl.dart';
@@ -7,11 +8,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../enums/vehicle_command_type.dart';
import '../enums/message_service_state.dart';
import '../enums/message_status.dart';
import '../models/chat_message.dart';
import '../models/vehicle_cmd.dart';
import '../services/chat_sse_service.dart'; import '../services/chat_sse_service.dart';
import '../services/classification_service.dart'; import '../services/classification_service.dart';
import '../services/control_recognition_service.dart'; import '../services/control_recognition_service.dart';
@@ -20,48 +16,72 @@ import '../services/control_recognition_service.dart';
import 'command_service.dart'; import 'command_service.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
// 用单例的模式创建
class MessageService extends ChangeNotifier { class MessageService extends ChangeNotifier {
static const MethodChannel _asrChannel = MethodChannel('com.example.ai_chat_assistant/asr'); static const MethodChannel _asrChannel = MethodChannel('com.example.ai_chat_assistant/ali_sdk');
static final MessageService _instance = MessageService._internal(); static MessageService? _instance;
factory MessageService() => _instance; static MessageService get instance {
return _instance ??= MessageService._internal();
}
// 提供工厂构造函数供 Provider 使用
factory MessageService() => instance;
Completer<String>? _asrCompleter; Completer<String>? _asrCompleter;
MessageService._internal() { MessageService._internal() {
_asrChannel.setMethodCallHandler((call) async { // 注册MethodChannel的handler
switch (call.method) { _asrChannel.setMethodCallHandler(_handleMethodCall);
case "onAsrResult": }
replaceMessage(
id: _latestUserMessageId!, @override
text: call.arguments, void dispose() {
status: MessageStatus.normal); // 取消注册MethodChannel的handler避免内存泄漏和意外回调
break; _asrChannel.setMethodCallHandler(null);
case "onAsrStop": _asrCompleter?.completeError('Disposed');
int index = findMessageIndexById(_latestUserMessageId!); _asrCompleter = null;
if (index == -1) { super.dispose();
return; }
}
final message = _messages[index]; // 提供真正的销毁方法(可选,用于应用退出时)
if (message.text.isEmpty) { void destroyInstance() {
removeMessageById(_latestUserMessageId!); _asrChannel.setMethodCallHandler(null);
return; _asrCompleter?.completeError('Destroyed');
} _asrCompleter = null;
if (_asrCompleter != null && !_asrCompleter!.isCompleted) { super.dispose();
_asrCompleter!.complete(messages.last.text); _instance = null;
} }
break;
} Future<dynamic> _handleMethodCall(MethodCall call) async {
}); switch (call.method) {
case "onAsrResult":
replaceMessage(
id: _latestUserMessageId!, text: call.arguments, status: MessageStatus.normal);
break;
case "onAsrStop":
int index = findMessageIndexById(_latestUserMessageId!);
if (index == -1) {
return;
}
final message = _messages[index];
if (message.text.isEmpty) {
removeMessageById(_latestUserMessageId!);
return;
}
if (_asrCompleter != null && !_asrCompleter!.isCompleted) {
_asrCompleter!.complete(messages.last.text);
}
break;
}
} }
final ChatSseService _chatSseService = ChatSseService(); final ChatSseService _chatSseService = ChatSseService();
// final LocalTtsService _ttsService = LocalTtsService(); // final LocalTtsService _ttsService = LocalTtsService();
// final AudioRecorderService _audioService = AudioRecorderService(); // final AudioRecorderService _audioService = AudioRecorderService();
// final VoiceRecognitionService _recognitionService = VoiceRecognitionService(); // final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
final TextClassificationService _classificationService = final TextClassificationService _classificationService = TextClassificationService();
TextClassificationService();
final VehicleCommandService _vehicleCommandService = VehicleCommandService(); final VehicleCommandService _vehicleCommandService = VehicleCommandService();
final List<ChatMessage> _messages = []; final List<ChatMessage> _messages = [];
@@ -106,7 +126,6 @@ class MessageService extends ChangeNotifier {
abortReply(); abortReply();
_latestUserMessageId = null; _latestUserMessageId = null;
_latestAssistantMessageId = null; _latestAssistantMessageId = null;
_isReplyAborted = false;
changeState(MessageServiceState.recording); changeState(MessageServiceState.recording);
_latestUserMessageId = addMessage("", true, MessageStatus.listening); _latestUserMessageId = addMessage("", true, MessageStatus.listening);
_asrChannel.invokeMethod("startAsr"); _asrChannel.invokeMethod("startAsr");
@@ -123,11 +142,7 @@ class MessageService extends ChangeNotifier {
String addMessage(String text, bool isUser, MessageStatus status) { String addMessage(String text, bool isUser, MessageStatus status) {
String uuid = Uuid().v1(); String uuid = Uuid().v1();
_messages.add(ChatMessage( _messages.add(ChatMessage(
id: uuid, id: uuid, text: text, isUser: isUser, timestamp: DateTime.now(), status: status));
text: text,
isUser: isUser,
timestamp: DateTime.now(),
status: status));
notifyListeners(); notifyListeners();
return uuid; return uuid;
} }
@@ -138,6 +153,7 @@ class MessageService extends ChangeNotifier {
return; return;
} }
try { try {
_isReplyAborted = false;
changeState(MessageServiceState.recognizing); changeState(MessageServiceState.recognizing);
_asrChannel.invokeMethod("stopAsr"); _asrChannel.invokeMethod("stopAsr");
_asrCompleter = Completer<String>(); _asrCompleter = Completer<String>();
@@ -226,8 +242,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted) { if (_isReplyAborted) {
return; return;
} }
final vehicleCommandResponse = final vehicleCommandResponse = await _vehicleCommandService.getCommandFromText(text);
await _vehicleCommandService.getCommandFromText(text);
if (vehicleCommandResponse == null) { if (vehicleCommandResponse == null) {
if (_isReplyAborted) { if (_isReplyAborted) {
return; return;
@@ -235,10 +250,7 @@ class MessageService extends ChangeNotifier {
String msg = isChinese String msg = isChinese
? "无法识别车辆控制命令,请重试" ? "无法识别车辆控制命令,请重试"
: "Cannot recognize the vehicle control command, please try again"; : "Cannot recognize the vehicle control command, please try again";
replaceMessage( replaceMessage(id: _latestAssistantMessageId!, text: msg, status: MessageStatus.normal);
id: _latestAssistantMessageId!,
text: msg,
status: MessageStatus.normal);
} else { } else {
if (_isReplyAborted) { if (_isReplyAborted) {
return; return;
@@ -259,8 +271,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted) { if (_isReplyAborted) {
return; return;
} }
if (command.type == VehicleCommandType.unknown || if (command.type == VehicleCommandType.unknown || command.error.isNotEmpty) {
command.error.isNotEmpty) {
continue; continue;
} }
if (command.type == VehicleCommandType.openAC) { if (command.type == VehicleCommandType.openAC) {
@@ -268,13 +279,13 @@ class MessageService extends ChangeNotifier {
} }
bool isSuccess; bool isSuccess;
if (containOpenAC && command.type == VehicleCommandType.changeACTemp) { if (containOpenAC && command.type == VehicleCommandType.changeACTemp) {
isSuccess = await Future.delayed(const Duration(milliseconds: 2000), isSuccess = await Future.delayed(
() => processCommand(command, isChinese)); const Duration(milliseconds: 2000), () => processCommand(command, isChinese));
} else { } else {
isSuccess = await processCommand(command, isChinese); isSuccess = await processCommand(command, isChinese);
} }
if (isSuccess) { if (isSuccess) {
successCommandList.add(command.type.name); successCommandList.add(isChinese ? command.type.chinese : command.type.english);
} }
} }
replaceMessage( replaceMessage(
@@ -285,8 +296,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted || successCommandList.isEmpty) { if (_isReplyAborted || successCommandList.isEmpty) {
return; return;
} }
String controlResponse = String controlResponse = await _vehicleCommandService.getControlResponse(successCommandList);
await _vehicleCommandService.getControlResponse(successCommandList);
if (_isReplyAborted || controlResponse.isEmpty) { if (_isReplyAborted || controlResponse.isEmpty) {
return; return;
} }
@@ -297,20 +307,22 @@ class MessageService extends ChangeNotifier {
Future<bool> processCommand(VehicleCommand command, bool isChinese) async { Future<bool> processCommand(VehicleCommand command, bool isChinese) async {
String msg = ""; String msg = "";
MessageStatus status = MessageStatus.normal; MessageStatus status = MessageStatus.normal;
final (isSuccess, result) = await CommandService.executeCommand( var commandObjc = VehicleCommand(type: command.type, params: command.params);
command.type, var client = AIChatAssistantManager.instance.commandClient;
params: command.params); if (client == null) {
msg = isChinese ? "车辆控制服务未初始化" : "Vehicle control service is not initialized";
status = MessageStatus.failure;
addMessage(msg, false, status);
return false;
}
final (isSuccess, result) = await client.executeCommand(commandObjc);
if (command.type == VehicleCommandType.locateCar) { if (command.type == VehicleCommandType.locateCar) {
msg = isChinese msg = isChinese ? "很抱歉,定位车辆位置失败" : "Sorry, locate the vehicle unsuccessfully";
? "很抱歉,定位车辆位置失败"
: "Sorry, locate the vehicle unsuccessfully";
status = MessageStatus.failure; status = MessageStatus.failure;
if (isSuccess && result != null && result.isNotEmpty) { if (isSuccess && result != null && result.isNotEmpty) {
String address = result['address']; String address = result['address'];
if (address.isNotEmpty) { if (address.isNotEmpty) {
msg = isChinese msg = isChinese ? "已为您找到车辆位置:\"$address\"" : "The vehicle location is \"$address\"";
? "已为您找到车辆位置:\"$address\""
: "The vehicle location is \"$address\"";
status = MessageStatus.success; status = MessageStatus.success;
} }
} }
@@ -386,8 +398,7 @@ class MessageService extends ChangeNotifier {
status: MessageStatus.completed, status: MessageStatus.completed,
); );
} else { } else {
replaceMessage( replaceMessage(id: messageId, text: responseText, status: MessageStatus.thinking);
id: messageId, text: responseText, status: MessageStatus.thinking);
} }
} catch (e) { } catch (e) {
abortReply(); abortReply();
@@ -401,8 +412,7 @@ class MessageService extends ChangeNotifier {
if (index == -1 || messages[index].status != MessageStatus.thinking) { if (index == -1 || messages[index].status != MessageStatus.thinking) {
return; return;
} }
replaceMessage( replaceMessage(id: _latestAssistantMessageId!, status: MessageStatus.aborted);
id: _latestAssistantMessageId!, status: MessageStatus.aborted);
} }
void removeMessageById(String id) { void removeMessageById(String id) {

View File

@@ -0,0 +1,31 @@
// assets_util.dart
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
/**
* AssetsUtil
* @description: 资源工具类
* @author guangfei.zhao
*/
class AssetsUtil {
static const String packageName = 'ai_chat_assistant';
static AssetImage getImage(String imageName) {
return AssetImage(
'assets/images/$imageName',
package: packageName,
);
}
static Widget getImageWidget(String imageName, {BoxFit? fit, double? width, double? height}) {
return Image.asset(
'assets/images/$imageName',
package: packageName,
fit: fit,
width: width,
height: height,
);
}
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
class TtsUtil { class TtsUtil {
static const MethodChannel _channel = static const MethodChannel _channel =
MethodChannel('com.example.ai_chat_assistant/tts'); MethodChannel('com.example.ai_chat_assistant/ali_sdk');
static Future<T?> execute<T>(String method, static Future<T?> execute<T>(String method,
[Map<Object, Object>? arguments]) { [Map<Object, Object>? arguments]) {

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
// 一个带洞的遮罩层点击洞外区域触发onTap事件
class HoleOverlay extends StatelessWidget {
final Offset iconPosition;
final double iconSize;
final VoidCallback? onTap;
final Widget? child;
const HoleOverlay({
super.key,
required this.iconPosition,
required this.iconSize,
this.onTap,
this.child,
});
@override
Widget build(BuildContext context) {
debugPrint('🔧 HoleOverlay build: iconPosition=$iconPosition, iconSize=$iconSize');
final holeRect = Rect.fromLTWH(
iconPosition.dx,
iconPosition.dy,
iconSize,
iconSize,
);
return CustomPaint(
painter: _HolePainter(
iconPosition: iconPosition,
iconSize: iconSize,
),
child: GestureDetector(
onTapDown: (details) {
final tapPosition = details.localPosition;
final isInHole = holeRect.contains(tapPosition);
debugPrint('🎯 HoleOverlay onTapDown: position=$tapPosition, holeRect=$holeRect, isInHole=$isInHole');
if (!isInHole) {
debugPrint('🔥 HoleOverlay: 点击洞外触发onTap');
onTap?.call();
} else {
debugPrint('🚫 HoleOverlay: 点击洞内不触发onTap');
}
},
child: child,
behavior: HitTestBehavior.translucent,
),
);
}
}
class _HolePainter extends CustomPainter {
final Offset iconPosition;
final double iconSize;
_HolePainter({
required this.iconPosition,
required this.iconSize,
});
@override
void paint(Canvas canvas, Size size) {
final holeRect = Rect.fromLTWH(
iconPosition.dx,
iconPosition.dy,
iconSize,
iconSize,
);
debugPrint('🎨 HolePainter paint: size=$size, holeRect=$holeRect');
// 使用 saveLayer 确保混合模式正确
canvas.saveLayer(Offset.zero & size, Paint());
// 画半透明背景
final backgroundPaint = Paint()
..color = Colors.black.withOpacity(0.3);
canvas.drawRect(Offset.zero & size, backgroundPaint);
debugPrint('🔲 HolePainter paint: 绘制背景完成');
// 挖洞:使用 BlendMode.clear
final holePaint = Paint()
..blendMode = BlendMode.clear;
canvas.drawOval(holeRect, holePaint);
debugPrint('⭕ HolePainter paint: 挖洞完成holeRect=$holeRect');
canvas.restore();
// 调试:画红色边框显示洞的位置
final debugPaint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawOval(holeRect, debugPaint);
debugPrint('🔴 HolePainter paint: 绘制调试边框完成');
}
@override
bool shouldRepaint(covariant _HolePainter oldDelegate) {
return oldDelegate.iconPosition != iconPosition ||
oldDelegate.iconSize != iconSize;
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../utils/assets_util.dart';
class AssistantAvatar extends StatelessWidget { class AssistantAvatar extends StatelessWidget {
final double width; final double width;
@@ -15,8 +16,8 @@ class AssistantAvatar extends StatelessWidget {
return SizedBox( return SizedBox(
width: width, width: width,
height: height, height: height,
child: Image.asset( child: AssetsUtil.getImageWidget(
'assets/images/avatar.png', 'avatar.png',
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
); );

View File

@@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'dart:ui';
import '../../widgets/chat_box.dart';
import 'package:provider/provider.dart';
import '../../services/message_service.dart';
import '../../widgets/gradient_background.dart';
import '../../utils/assets_util.dart';
import '../../pages/full_screen.dart';
class ChatWindowContent extends StatefulWidget {
final AnimationController? animationController;
final double? minHeight;
final double? maxHeight;
final double? chatWidth;
const ChatWindowContent({
super.key,
this.animationController,
this.minHeight,
this.maxHeight,
this.chatWidth,
});
@override
State<ChatWindowContent> createState() => _ChatWindowContentState();
}
class _ChatWindowContentState extends State<ChatWindowContent> {
final ScrollController _scrollController = ScrollController();
int _lastMessageCount = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final messageService = Provider.of<MessageService>(context, listen: false);
messageService.removeNonListeningMessages();
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
void _openFullScreen() async {
final messageService = context.read<MessageService>();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: messageService, // 传递同一个单例实例
child: const FullScreenPage(),
),
),
);
}
@override
Widget build(BuildContext context) {
final minHeight = widget.minHeight ?? 0;
final maxHeight = widget.maxHeight ?? double.infinity;
final chatWidth = widget.chatWidth ?? double.infinity;
final animationController = widget.animationController;
return Consumer<MessageService>(
builder: (context, messageService, child) {
final messageCount = messageService.messages.length;
if (messageCount > _lastMessageCount) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
_lastMessageCount = messageCount;
var chatContent = _buildChatContent(messageService);
// 包裹弹窗动画
if (animationController != null) {
Widget content = FadeTransition(
opacity: animationController,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeOutQuart,
)),
child: TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.0),
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
child: Container(
width: chatWidth,
constraints: BoxConstraints(
minHeight: minHeight,
maxHeight: maxHeight,
),
child: child,
),
);
},
child: chatContent,
),
),
);
}
Widget content = Container(
width: chatWidth,
constraints: BoxConstraints(
minHeight: minHeight,
maxHeight: maxHeight,
),
child: chatContent,
);
return content;
},
);
}
Widget _buildChatContent(MessageService messageService) {
Widget content = ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: GradientBackground(
colors: const [
Color(0XBF3B0A3F),
Color(0xBF0E0E24),
Color(0xBF0C0B33),
],
borderRadius: BorderRadius.circular(6),
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(top: 50, left: 6, right: 6, bottom: 30),
child: LayoutBuilder(
builder: (context, boxConstraints) {
return ChatBox(
scrollController: _scrollController,
messages: messageService.messages,
);
},
),
),
Positioned(
top: 6,
right: 6,
child: IconButton(
icon: AssetsUtil.getImageWidget(
'open_in_full.png',
width: 24,
height: 24,
),
onPressed: _openFullScreen,
padding: EdgeInsets.zero,
),
),
],
),
),
),
);
return content;
}
}

View File

@@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
import '../services/message_service.dart'; import '../services/message_service.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import '../utils/assets_util.dart';
class ChatBubble extends StatefulWidget { class ChatBubble extends StatefulWidget {
final ChatMessage message; final ChatMessage message;
@@ -108,7 +109,7 @@ class _ChatBubbleState extends State<ChatBubble> {
case MessageStatus.listening: case MessageStatus.listening:
case MessageStatus.recognizing: case MessageStatus.recognizing:
case MessageStatus.thinking: case MessageStatus.thinking:
icon = RotatingImage(imagePath: 'assets/images/thinking_circle.png'); icon = RotatingImage(imagePath: 'thinking_circle.png');
color = Colors.white; color = Colors.white;
break; break;
case MessageStatus.executing: case MessageStatus.executing:
@@ -123,7 +124,11 @@ class _ChatBubbleState extends State<ChatBubble> {
break; break;
case MessageStatus.completed: case MessageStatus.completed:
case MessageStatus.success: case MessageStatus.success:
icon = Image.asset('assets/images/checked.png', width: 20, height: 20); icon = AssetsUtil.getImageWidget(
'checked.png',
width: 20,
height: 20,
);
color = Colors.white; color = Colors.white;
break; break;
case MessageStatus.failure: case MessageStatus.failure:
@@ -301,8 +306,9 @@ class _ChatBubbleState extends State<ChatBubble> {
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 12), padding: const EdgeInsets.only(left: 12),
child: Image.asset('assets/images/copy.png', child: AssetsUtil.getImageWidget('copy.png',
width: 22, height: 22), width: 22,
height: 22),
), ),
), ),
InkWell( InkWell(
@@ -315,10 +321,12 @@ class _ChatBubbleState extends State<ChatBubble> {
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 12), padding: const EdgeInsets.only(left: 12),
child: _liked child: _liked
? Image.asset('assets/images/liked2.png', ? AssetsUtil.getImageWidget('liked2.png',
width: 22, height: 22) width: 22,
: Image.asset('assets/images/liked1.png', height: 22)
width: 22, height: 22), : AssetsUtil.getImageWidget('liked1.png',
width: 22,
height: 22),
), ),
), ),
InkWell( InkWell(
@@ -331,10 +339,12 @@ class _ChatBubbleState extends State<ChatBubble> {
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 12), padding: const EdgeInsets.only(left: 12),
child: _disliked child: _disliked
? Image.asset('assets/images/disliked2.png', ? AssetsUtil.getImageWidget('disliked2.png',
width: 22, height: 22) width: 22,
: Image.asset('assets/images/disliked1.png', height: 22)
width: 22, height: 22)), : AssetsUtil.getImageWidget('disliked1.png',
width: 22,
height: 22)),
), ),
], ],
), ),

View File

@@ -0,0 +1,250 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/message_service.dart';
import '../pages/full_screen.dart';
import 'floating_icon_with_wave.dart';
import 'package:basic_intl/intl.dart';
import '../utils/assets_util.dart';
class ChatFloatingIcon extends StatefulWidget {
final double? iconSize;
final Widget? icon;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final VoidCallback? onLongPressUp;
final ValueChanged<LongPressEndDetails>? onLongPressEnd;
final VoidCallback? onDragStart;
final VoidCallback? onDragEnd;
final ValueChanged<Offset>? onPositionChanged;
const ChatFloatingIcon({
super.key,
this.iconSize,
this.icon,
this.onTap,
this.onLongPress,
this.onLongPressUp,
this.onLongPressEnd,
this.onDragStart,
this.onDragEnd,
this.onPositionChanged,
});
@override
State<ChatFloatingIcon> createState() => _ChatFloatingIconState();
}
class _ChatFloatingIconState extends State<ChatFloatingIcon> with TickerProviderStateMixin {
double iconSize = 0.0;
Offset _position = const Offset(10, 120);
bool _isDragging = false;
bool _isAnimating = false;
Offset _targetPosition = const Offset(10, 120);
// 水波纹动画控制器
late AnimationController _waveAnimationController;
// 图片切换定时器
Timer? _imageTimer;
// 添加长按状态标记
bool _isLongPressing = false;
// 图片切换相关
int _imageIndex = 0;
late final List<String> _iconImages = [
'ai1_hd.png',
'ai0_hd.png',
];
late final List<String> _iconImagesEn = [
'ai1_hd_en.png',
'ai0_hd_en.png',
];
@override
void initState() {
super.initState();
iconSize = widget.iconSize ?? 80.0;
_waveAnimationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
// 如果没有提供自定义图标,则使用默认的图片切换逻辑
if (widget.icon == null) {
_imageTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (mounted && !_isDragging) {
setState(() {
_imageIndex = (_imageIndex + 1) % _iconImages.length;
});
}
});
}
}
@override
void dispose() {
_waveAnimationController.dispose();
_imageTimer?.cancel();
super.dispose();
}
// 更新位置
void _updatePosition(Offset delta) {
if (!_isDragging) return;
setState(() {
final screenSize = MediaQuery.of(context).size;
double newX = (_position.dx - delta.dx).clamp(0.0, screenSize.width - iconSize);
double newY = (_position.dy - delta.dy).clamp(0.0, screenSize.height - iconSize);
_position = Offset(newX, newY);
});
// 通知父组件位置变化
widget.onPositionChanged?.call(_position);
}
// 吸附到屏幕边缘
void _snapToEdge() {
final screenSize = MediaQuery.of(context).size;
final centerX = screenSize.width / 2;
// 判断应该吸到左边还是右边
final shouldSnapToLeft = _position.dx < centerX - iconSize / 2;
final targetX = shouldSnapToLeft ? 0.0 : screenSize.width - iconSize;
setState(() {
_targetPosition = Offset(targetX, _position.dy);
_isAnimating = true;
});
// 使用定时器在动画完成后更新状态
Timer(const Duration(milliseconds: 100), () {
if (mounted) {
setState(() {
_position = _targetPosition;
_isAnimating = false;
});
// 通知父组件位置变化
widget.onPositionChanged?.call(_position);
}
});
}
void _onPanStart(DragStartDetails details) {
_isDragging = true;
_waveAnimationController.stop();
widget.onDragStart?.call();
}
void _onPanEnd(DragEndDetails details) {
_isDragging = false;
_snapToEdge();
widget.onDragEnd?.call();
}
// 显示全屏界面
void _showFullScreen() async {
final messageService = MessageService.instance;
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: messageService,
child: const FullScreenPage(),
),
),
);
}
Widget _buildIconContent(MessageService messageService) {
// 如果提供了自定义图标,优先使用
if (widget.icon != null) {
return widget.icon!;
}
// 使用默认的图标逻辑
if (messageService.isRecording) {
return FloatingIconWithWave(
animationController: _waveAnimationController,
iconSize: iconSize,
waveColor: Colors.white,
);
} else {
return AssetsUtil.getImageWidget(
Intl.getCurrentLocale().startsWith('zh')
? _iconImages[_imageIndex]
: _iconImagesEn[_imageIndex],
width: iconSize,
height: iconSize,
);
}
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: MessageService.instance,
child: AnimatedPositioned(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutBack,
bottom: _isAnimating ? _targetPosition.dy : _position.dy,
right: _isAnimating ? _targetPosition.dx : _position.dx,
child: Consumer<MessageService>(
builder: (context, messageService, child) {
return GestureDetector(
onTap: widget.onTap ?? _showFullScreen,
onLongPress: () async {
_isLongPressing = true;
_waveAnimationController.repeat();
if (widget.onLongPress != null) {
widget.onLongPress!();
} else {
await messageService.startVoiceInput();
}
},
onLongPressUp: () async {
_isLongPressing = false;
_waveAnimationController.stop();
if (widget.onLongPressUp != null) {
widget.onLongPressUp!();
} else {
await messageService.stopAndProcessVoiceInput();
}
},
onLongPressEnd: (details) {
_isLongPressing = false;
widget.onLongPressEnd?.call(details);
},
onPanStart: (details) {
if (!_isLongPressing) {
_onPanStart(details);
}
},
onPanUpdate: (details) {
if (_isDragging && !_isLongPressing) {
_updatePosition(details.delta);
}
},
onPanEnd: (details) {
if (!_isLongPressing) {
_onPanEnd(details);
}
},
child: AnimatedScale(
scale: _isDragging ? 1.1 : 1.0,
duration: const Duration(milliseconds: 150),
child: _buildIconContent(messageService),
),
);
},
),
),
);
}
}

View File

@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import '../services/message_service.dart'; import '../services/message_service.dart';
import '../utils/common_util.dart'; import '../utils/common_util.dart';
import 'large_audio_wave.dart'; import 'large_audio_wave.dart';
import '../utils/assets_util.dart';
class ChatFooter extends StatefulWidget { class ChatFooter extends StatefulWidget {
final VoidCallback? onClear; final VoidCallback? onClear;
@@ -48,8 +49,8 @@ class _ChatFooterState extends State<ChatFooter>
padding: const EdgeInsets.only(left: 16, bottom: 12), padding: const EdgeInsets.only(left: 16, bottom: 12),
child: GestureDetector( child: GestureDetector(
onTap: widget.onClear, onTap: widget.onClear,
child: Image.asset( child: AssetsUtil.getImageWidget(
'assets/images/delete.png', 'delete.png',
width: 24, width: 24,
height: 24, height: 24,
), ),
@@ -135,9 +136,11 @@ class _ChatFooterState extends State<ChatFooter>
Padding( Padding(
padding: const EdgeInsets.only(bottom: 12, right: 16), padding: const EdgeInsets.only(bottom: 12, right: 16),
child: GestureDetector( child: GestureDetector(
onTap: () {}, onTap: () {
child: Image.asset(
'assets/images/keyboard_mini.png', },
child: AssetsUtil.getImageWidget(
'keyboard_mini.png',
width: 24, width: 24,
height: 24, height: 24,
), ),

View File

@@ -44,7 +44,7 @@ class ChatHeader extends StatelessWidget {
? '您的专属看、选、买、用车助手' ? '您的专属看、选、买、用车助手'
: 'Your exclusive assistant', : 'Your exclusive assistant',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 11,
color: Colors.white70, color: Colors.white70,
), ),
), ),

279
lib/widgets/chat_popup.dart Normal file
View File

@@ -0,0 +1,279 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../pages/full_screen.dart';
import '../services/message_service.dart';
import 'chat/chat_window_content.dart';
import 'chat_floating_icon.dart';
class ChatPopup extends StatefulWidget {
final double? iconSize;
final Widget? icon;
final Size chatSize;
final ChatFloatingIcon? child;
const ChatPopup({
super.key,
this.child,
this.iconSize,
this.icon,
this.chatSize = const Size(320, 450),
});
@override
State<ChatPopup> createState() => _ChatPopupState();
}
class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMixin {
Offset _iconPosition = const Offset(10, 120);
final double _iconSizeDefault = 80.0;
bool _isShowingPopup = false;
late AnimationController _partScreenAnimationController;
@override
void initState() {
super.initState();
_partScreenAnimationController = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
);
}
@override
void dispose() {
_partScreenAnimationController.dispose();
super.dispose();
}
void _insertOverlay() {
if (_isShowingPopup) return;
setState(() {
_isShowingPopup = true;
});
_partScreenAnimationController.forward();
}
void _removeOverlay() {
if (!_isShowingPopup) return;
_partScreenAnimationController.reverse().then((_) {
if (mounted) {
setState(() {
_isShowingPopup = false;
});
}
});
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: MessageService.instance,
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
// 1. 底层:弹窗内容(只有在显示时才渲染)
if (_isShowingPopup) ...[
_buildPopupBackground(),
_buildPopupContent(constraints),
],
// 2. 顶层FloatingIcon始终在最上层事件不被遮挡
_buildFloatingIcon(),
],
);
},
),
);
}
Widget _buildPopupBackground() {
return Positioned.fill(
child: GestureDetector(
onTap: () {
final messageService = MessageService.instance;
_removeOverlay();
messageService.abortReply();
messageService.initializeEmpty();
},
child: SizedBox.expand()
),
);
}
Widget _buildPopupContent(BoxConstraints constraints) {
final position = _calculatePosition(constraints);
final double minHeight = constraints.maxHeight * 0.16;
final double maxHeight = constraints.maxHeight * 0.4;
final double chatWidth = constraints.maxWidth * 0.94;
return Positioned(
left: position.left,
right: position.right,
bottom: position.bottom,
child: ChatWindowContent(
animationController: _partScreenAnimationController,
minHeight: minHeight,
maxHeight: maxHeight,
chatWidth: chatWidth,
),
);
}
Widget _buildFloatingIcon() {
// 如果外部传入了 child优先使用它的属性
if (widget.child != null) {
return ChatFloatingIcon(
iconSize: widget.child!.iconSize ?? widget.iconSize ?? _iconSizeDefault,
icon: widget.child!.icon ?? widget.icon,
onTap: () async {
debugPrint('🖱️ FloatingIcon onTap triggered! (using child properties)');
// 先执行外部传入的 onTap如果有
if (widget.child!.onTap != null) {
widget.child!.onTap!();
} else {
// 默认行为:关闭弹窗并打开全屏
final messageService = context.read<MessageService>();
_removeOverlay();
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: messageService,
child: const FullScreenPage(),
),
),
);
}
},
onLongPress: () async {
debugPrint('⏳ FloatingIcon onLongPress triggered! (using child properties)');
// 先执行外部传入的 onLongPress如果有
if (widget.child!.onLongPress != null) {
widget.child!.onLongPress!();
}
// 执行内部业务逻辑
final messageService = MessageService.instance;
_insertOverlay();
await messageService.startVoiceInput();
},
onLongPressUp: () async {
debugPrint('⏫ FloatingIcon onLongPressUp triggered! (using child properties)');
// 先执行外部传入的 onLongPressUp如果有
if (widget.child!.onLongPressUp != null) {
widget.child!.onLongPressUp!();
}
// 执行内部业务逻辑
final messageService = MessageService.instance;
await messageService.stopAndProcessVoiceInput();
final hasMessage = messageService.messages.isNotEmpty;
if (!hasMessage) {
_removeOverlay();
}
},
onPositionChanged: (position) {
debugPrint('📍 FloatingIcon position changed: $position (using child properties)');
// 先执行外部传入的 onPositionChanged如果有
if (widget.child!.onPositionChanged != null) {
widget.child!.onPositionChanged!(position);
}
// 执行内部业务逻辑
setState(() {
_iconPosition = position;
});
},
// 传递其他可能的属性
onLongPressEnd: widget.child!.onLongPressEnd,
onDragStart: widget.child!.onDragStart,
onDragEnd: widget.child!.onDragEnd,
);
}
// 如果没有传入 child使用原来的逻辑
return ChatFloatingIcon(
iconSize: widget.iconSize ?? _iconSizeDefault,
icon: widget.icon,
onTap: () async {
debugPrint('🖱️ FloatingIcon onTap triggered!');
final messageService = context.read<MessageService>();
_removeOverlay();
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: messageService, // 传递同一个单例实例
child: const FullScreenPage(),
),
),
);
},
onLongPress: () async {
debugPrint('⏳ FloatingIcon onLongPress triggered!');
final messageService = MessageService.instance;
_insertOverlay();
await messageService.startVoiceInput();
},
onLongPressUp: () async {
debugPrint('⏫ FloatingIcon onLongPressUp triggered!');
final messageService = MessageService.instance;
await messageService.stopAndProcessVoiceInput();
final hasMessage = messageService.messages.isNotEmpty;
if (!hasMessage) {
_removeOverlay();
}
},
onPositionChanged: (position) {
debugPrint('📍 FloatingIcon position changed: $position');
setState(() {
_iconPosition = position;
});
},
);
}
// 计算PartScreen的位置使其显示在FloatingIcon附近
EdgeInsets _calculatePosition(BoxConstraints constraints) {
final screenWidth = constraints.maxWidth;
final screenHeight = constraints.maxHeight;
final chatWidth = screenWidth * 0.94;
final chatHeight = screenHeight * 0.4;
// 1. 先将icon的right/bottom转为屏幕坐标icon左下角
final iconLeft = screenWidth - _iconPosition.dx - (widget.iconSize ?? _iconSizeDefault);
final iconBottom = _iconPosition.dy;
final iconTop = iconBottom + (widget.iconSize ?? _iconSizeDefault);
final iconCenterX = iconLeft + (widget.iconSize ?? _iconSizeDefault) / 2;
// 判断FloatingIcon在屏幕的哪一边
final isOnRightSide = iconCenterX > screenWidth / 2;
// 计算水平位置
double leftPadding;
if (isOnRightSide) {
// 图标在右边,聊天框显示在左边
leftPadding = (screenWidth - chatWidth) * 0.1;
} else {
// 图标在左边,聊天框显示在右边
leftPadding = screenWidth - chatWidth - (screenWidth - chatWidth) * 0.1;
}
// 优先尝试放在icon上方
double chatBottom = iconTop + 12; // 聊天框底部距离icon顶部12px
double chatTop = screenHeight - chatBottom - chatHeight;
// 如果上方空间不足则放在icon下方
if (chatTop < 0) {
chatBottom = iconBottom - 12 - chatHeight / 2; // 聊天框顶部距离icon底部12px
// 如果下方也不足,则贴底
if (chatBottom < 0) {
chatBottom = 20; // 距离底部20px
}
}
return EdgeInsets.only(
left: leftPadding,
right: screenWidth - leftPadding - chatWidth,
bottom: chatBottom,
);
}
}

View File

@@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../services/message_service.dart'; import '../services/message_service.dart';
import '../screens/full_screen.dart'; import '../pages/full_screen.dart';
import '../screens/part_screen.dart'; import '../screens/part_screen.dart';
import 'floating_icon_with_wave.dart'; import 'floating_icon_with_wave.dart';
import 'dart:async'; // 添加此行 import 'dart:async';
import 'package:basic_intl/intl.dart'; import 'package:basic_intl/intl.dart';
import '../utils/assets_util.dart';
class FloatingIcon extends StatefulWidget { class FloatingIcon extends StatefulWidget {
const FloatingIcon({super.key}); const FloatingIcon({super.key});
@@ -14,26 +15,35 @@ class FloatingIcon extends StatefulWidget {
State<FloatingIcon> createState() => _FloatingIconState(); State<FloatingIcon> createState() => _FloatingIconState();
} }
class _FloatingIconState extends State<FloatingIcon> class _FloatingIconState extends State<FloatingIcon> with TickerProviderStateMixin {
with SingleTickerProviderStateMixin {
Offset _position = const Offset(10, 120); Offset _position = const Offset(10, 120);
final iconSize = 80.0; final iconSize = 80.0;
bool _isShowPartScreen = false; bool _isShowPartScreen = false;
bool _isDragging = false;
bool _isAnimating = false;
Offset _targetPosition = const Offset(10, 120);
// 水波纹动画控制器
late AnimationController _waveAnimationController; late AnimationController _waveAnimationController;
// PartScreen 显示动画控制器
late AnimationController _partScreenAnimationController;
// 图片切换定时器
Timer? _imageTimer;
// 添加长按状态标记
bool _isLongPressing = false;
// 新增:图片切换相关 // 图片切换相关
int _imageIndex = 0; int _imageIndex = 0;
late final List<String> _iconImages = [ late final List<String> _iconImages = [
'assets/images/ai1_hd.png', 'ai1_hd.png',
'assets/images/ai0_hd.png', 'ai0_hd.png',
]; ];
late final List<String> _iconImagesEn = [ late final List<String> _iconImagesEn = [
'assets/images/ai1_hd_en.png', 'ai1_hd_en.png',
'assets/images/ai0_hd_en.png', 'ai0_hd_en.png',
]; ];
Timer? _imageTimer; // 用于定时切换图片
@override @override
void initState() { void initState() {
@@ -42,18 +52,28 @@ class _FloatingIconState extends State<FloatingIcon>
duration: const Duration(milliseconds: 1000), duration: const Duration(milliseconds: 1000),
vsync: this, vsync: this,
); );
// 使用Timer.periodic定时切换图片
// PartScreen动画控制器
_partScreenAnimationController = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
);
// 图片切换定时器
_imageTimer = Timer.periodic(const Duration(seconds: 3), (timer) { _imageTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
setState(() { if (mounted && !_isDragging) {
_imageIndex = (_imageIndex + 1) % _iconImages.length; setState(() {
}); _imageIndex = (_imageIndex + 1) % _iconImages.length;
});
}
}); });
} }
@override @override
void dispose() { void dispose() {
_waveAnimationController.dispose(); _waveAnimationController.dispose();
_imageTimer?.cancel(); // 释放Timer _partScreenAnimationController.dispose();
_imageTimer?.cancel();
super.dispose(); super.dispose();
} }
@@ -61,67 +81,141 @@ class _FloatingIconState extends State<FloatingIcon>
setState(() { setState(() {
_isShowPartScreen = true; _isShowPartScreen = true;
}); });
_partScreenAnimationController.forward();
} }
void _hidePartScreen() { void _hidePartScreen() {
setState(() { _partScreenAnimationController.reverse().then((_) {
_isShowPartScreen = false; if (mounted) {
setState(() {
_isShowPartScreen = false;
});
}
}); });
} }
// 显示全屏界面
void _showFullScreen() async { void _showFullScreen() async {
_hidePartScreen(); _hidePartScreen();
final messageService = MessageService.instance;
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const FullScreen(), builder: (context) => ChangeNotifierProvider.value(
value: messageService, // 传递同一个单例实例
child: const FullScreenPage(),
),
), ),
); );
} }
// 更新位置
void _updatePosition(Offset delta) { void _updatePosition(Offset delta) {
if (!_isDragging) return;
setState(() { setState(() {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
double newX = double newX = (_position.dx - delta.dx).clamp(0.0, screenSize.width - iconSize);
(_position.dx - delta.dx).clamp(0.0, screenSize.width - iconSize); double newY = (_position.dy - delta.dy).clamp(0.0, screenSize.height - iconSize);
double newY =
(_position.dy - delta.dy).clamp(0.0, screenSize.height - iconSize);
_position = Offset(newX, newY); _position = Offset(newX, newY);
}); });
} }
// 吸附到屏幕边缘
void _snapToEdge() {
final screenSize = MediaQuery.of(context).size;
final centerX = screenSize.width / 2;
// 判断应该吸到左边还是右边
final shouldSnapToLeft = _position.dx < centerX - iconSize / 2;
final targetX = shouldSnapToLeft ? 0.0 : screenSize.width - iconSize;
setState(() {
_targetPosition = Offset(targetX, _position.dy);
_isAnimating = true;
});
// 使用定时器在动画完成后更新状态
Timer(const Duration(milliseconds: 100), () {
if (mounted) {
setState(() {
_position = _targetPosition;
_isAnimating = false;
});
}
});
}
void _onPanStart(DragStartDetails details) {
_isDragging = true;
_waveAnimationController.stop();
}
void _onPanEnd(DragEndDetails details) {
_isDragging = false;
_snapToEdge();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 奇怪的代码不应该放在build里面
// WidgetsBinding.instance.addPostFrameCallback((_) {
// if (messageService.isRecording) {
// if (!_waveAnimationController.isAnimating) {
// _waveAnimationController.repeat();
// }
// } else {
// if (_waveAnimationController.isAnimating) {
// _waveAnimationController.stop();
// }
// }
// });
return ChangeNotifierProvider.value(
value: MessageService.instance,
child: _buildFloatingIcon(),
);
}
Widget _buildFloatingIcon() {
return Stack( return Stack(
children: [ children: [
if (_isShowPartScreen) if (_isShowPartScreen)
PartScreen( FadeTransition(
onHide: _hidePartScreen, opacity: _partScreenAnimationController,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _partScreenAnimationController,
curve: Curves.easeOutQuart,
)),
child: PartScreen(
onHide: _hidePartScreen,
floatingIconPosition: _position,
iconSize: iconSize,
)),
), ),
Positioned( AnimatedPositioned(
bottom: _position.dy, duration: const Duration(milliseconds: 250),
right: _position.dx, curve: Curves.easeOutBack,
bottom: _isAnimating ? _targetPosition.dy : _position.dy,
right: _isAnimating ? _targetPosition.dx : _position.dx,
child: Consumer<MessageService>( child: Consumer<MessageService>(
builder: (context, messageService, child) { builder: (context, messageService, child) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (messageService.isRecording) {
if (!_waveAnimationController.isAnimating) {
_waveAnimationController.repeat();
}
} else {
if (_waveAnimationController.isAnimating) {
_waveAnimationController.stop();
}
}
});
return GestureDetector( return GestureDetector(
onTap: _showFullScreen, onTap: _showFullScreen,
onLongPress: () async { onLongPress: () async {
debugPrint('Long press');
_isLongPressing = true; // 标记开始长按
_showPartScreen(); _showPartScreen();
_waveAnimationController.repeat(); _waveAnimationController.repeat();
await messageService.startVoiceInput(); await messageService.startVoiceInput();
}, },
onLongPressUp: () async { onLongPressUp: () async {
debugPrint('Long press up');
_isLongPressing = false; // 清除长按标记
_waveAnimationController.stop(); _waveAnimationController.stop();
await messageService.stopAndProcessVoiceInput(); await messageService.stopAndProcessVoiceInput();
final hasMessage = messageService.messages.isNotEmpty; final hasMessage = messageService.messages.isNotEmpty;
@@ -129,23 +223,48 @@ class _FloatingIconState extends State<FloatingIcon>
_hidePartScreen(); _hidePartScreen();
} }
}, },
onPanUpdate: (details) => _updatePosition(details.delta), onLongPressEnd: (d) {
child: messageService.isRecording debugPrint('Long press end $d');
? FloatingIconWithWave( _isLongPressing = false; // 清除长按标记
animationController: _waveAnimationController, },
iconSize: iconSize, onPanStart: (details) {
waveColor: Colors.white, // 只有在非长按状态下才允许拖拽
) if (!_isLongPressing) {
: Image.asset( _onPanStart(details);
Intl.getCurrentLocale().startsWith('zh') }
? _iconImages[_imageIndex]:_iconImagesEn[_imageIndex], },
width: iconSize, onPanUpdate: (details) {
height: iconSize, // 只有在拖拽状态下才更新位置
), if (_isDragging && !_isLongPressing) {
_updatePosition(details.delta);
}
},
onPanEnd: (details) {
if (!_isLongPressing) {
_onPanEnd(details);
}
},
child: AnimatedScale(
scale: _isDragging ? 1.1 : 1.0,
duration: const Duration(milliseconds: 150),
child: messageService.isRecording
? FloatingIconWithWave(
animationController: _waveAnimationController,
iconSize: iconSize,
waveColor: Colors.white,
)
: AssetsUtil.getImageWidget(
Intl.getCurrentLocale().startsWith('zh')
? _iconImages[_imageIndex]
: _iconImagesEn[_imageIndex],
width: iconSize,
height: iconSize,
),
),
); );
}, },
), ),
), )
], ],
); );
} }

View File

@@ -1,3 +1,4 @@
import 'package:ai_chat_assistant/utils/assets_util.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'mini_audio_wave.dart'; import 'mini_audio_wave.dart';
@@ -31,8 +32,8 @@ class FloatingIconWithWave extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
// 背景图片 // 背景图片
Image.asset( AssetsUtil.getImageWidget(
'assets/images/ai_hd_clean.png', 'ai_hd_clean.png',
width: iconSize, width: iconSize,
height: iconSize, height: iconSize,
fit: BoxFit.contain, fit: BoxFit.contain,

View File

@@ -1,15 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../utils/assets_util.dart';
class RotatingImage extends StatefulWidget { class RotatingImage extends StatefulWidget {
final String imagePath; final String imagePath;
final double size; final double size;
final Duration duration; final Duration duration;
final String? package; // 添加这个参数
const RotatingImage({ const RotatingImage({
Key? key, Key? key,
required this.imagePath, required this.imagePath,
this.size = 20, this.size = 20,
this.duration = const Duration(seconds: 3) this.duration = const Duration(seconds: 3),
this.package = 'ai_chat_assistant', // 默认值
}) : super(key: key); }) : super(key: key);
@override @override
@@ -39,10 +43,10 @@ class _RotatingImageState extends State<RotatingImage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RotationTransition( return RotationTransition(
turns: _controller, turns: _controller,
child: Image.asset( child: AssetsUtil.getImageWidget(
widget.imagePath, widget.imagePath,
width: widget.size, width: widget.size,
height: widget.size height: widget.size,
), ),
); );
} }

View File

@@ -0,0 +1,3 @@
library basic_intl;
export 'src/intl.dart';

View File

@@ -0,0 +1,3 @@
library basic_intl;
export 'src/intl.dart';

View File

@@ -0,0 +1,32 @@
import 'dart:ui';
class Intl {
static String _currentLocale = 'zh_CN';
/// 获取当前语言环境
static String getCurrentLocale() {
// 尝试从系统获取语言环境
try {
final systemLocale = PlatformDispatcher.instance.locale;
_currentLocale = '${systemLocale.languageCode}_${systemLocale.countryCode ?? 'CN'}';
} catch (e) {
_currentLocale = 'zh_CN';
}
return _currentLocale;
}
/// 设置当前语言环境
static void setCurrentLocale(String locale) {
_currentLocale = locale;
}
/// 判断是否为中文环境
static bool isChineseLocale() {
return getCurrentLocale().startsWith('zh');
}
/// 判断是否为英文环境
static bool isEnglishLocale() {
return getCurrentLocale().startsWith('en');
}
}

View File

@@ -0,0 +1,189 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.0"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.15.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.3"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.flutter-io.cn"
source: hosted
version: "14.3.0"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

View File

@@ -0,0 +1,16 @@
name: basic_intl
description: Basic internationalization utilities for ai_chat_assistant
version: 0.2.0
environment:
sdk: ^3.5.0
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:

View File

@@ -6,7 +6,7 @@ packages:
description: description:
name: args name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.7.0" version: "2.7.0"
async: async:
@@ -14,7 +14,7 @@ packages:
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
audioplayers: audioplayers:
@@ -22,7 +22,7 @@ packages:
description: description:
name: audioplayers name: audioplayers
sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.2.1" version: "5.2.1"
audioplayers_android: audioplayers_android:
@@ -30,7 +30,7 @@ packages:
description: description:
name: audioplayers_android name: audioplayers_android
sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.0.3" version: "4.0.3"
audioplayers_darwin: audioplayers_darwin:
@@ -38,7 +38,7 @@ packages:
description: description:
name: audioplayers_darwin name: audioplayers_darwin
sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.0.2" version: "5.0.2"
audioplayers_linux: audioplayers_linux:
@@ -46,7 +46,7 @@ packages:
description: description:
name: audioplayers_linux name: audioplayers_linux
sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
audioplayers_platform_interface: audioplayers_platform_interface:
@@ -54,7 +54,7 @@ packages:
description: description:
name: audioplayers_platform_interface name: audioplayers_platform_interface
sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
audioplayers_web: audioplayers_web:
@@ -62,7 +62,7 @@ packages:
description: description:
name: audioplayers_web name: audioplayers_web
sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
audioplayers_windows: audioplayers_windows:
@@ -70,39 +70,30 @@ packages:
description: description:
name: audioplayers_windows name: audioplayers_windows
sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
basic_intl: basic_intl:
dependency: "direct main" dependency: "direct main"
description: description:
name: basic_intl path: "packages/basic_intl"
sha256: "15ba8447fb069bd80f0b0c74c71faf5dea45874e9566a68e4e30c897ab2b07c4" relative: true
url: "http://175.24.250.68:4000" source: path
source: hosted
version: "0.2.0" version: "0.2.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "http://175.24.250.68:4000"
source: hosted
version: "1.1.2"
collection: collection:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.19.0" version: "1.19.0"
crypto: crypto:
@@ -110,7 +101,7 @@ packages:
description: description:
name: crypto name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.6" version: "3.0.6"
ffi: ffi:
@@ -118,7 +109,7 @@ packages:
description: description:
name: ffi name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
file: file:
@@ -126,7 +117,7 @@ packages:
description: description:
name: file name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
flutter: flutter:
@@ -139,7 +130,7 @@ packages:
description: description:
name: flutter_lints name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_markdown: flutter_markdown:
@@ -147,7 +138,7 @@ packages:
description: description:
name: flutter_markdown name: flutter_markdown
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.7.7+1" version: "0.7.7+1"
flutter_tts: flutter_tts:
@@ -155,7 +146,7 @@ packages:
description: description:
name: flutter_tts name: flutter_tts
sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4 sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.2.3" version: "4.2.3"
flutter_web_plugins: flutter_web_plugins:
@@ -168,7 +159,7 @@ packages:
description: description:
name: fluttertoast name: fluttertoast
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "8.2.12" version: "8.2.12"
http: http:
@@ -176,7 +167,7 @@ packages:
description: description:
name: http name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
http_parser: http_parser:
@@ -184,7 +175,7 @@ packages:
description: description:
name: http_parser name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
js: js:
@@ -192,7 +183,7 @@ packages:
description: description:
name: js name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.6.7" version: "0.6.7"
lints: lints:
@@ -200,7 +191,7 @@ packages:
description: description:
name: lints name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.1.1" version: "5.1.1"
markdown: markdown:
@@ -208,7 +199,7 @@ packages:
description: description:
name: markdown name: markdown
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.3.0" version: "7.3.0"
material_color_utilities: material_color_utilities:
@@ -216,15 +207,15 @@ packages:
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.11.1" version: "0.11.1"
meta: meta:
dependency: transitive dependency: "direct main"
description: description:
name: meta name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.15.0" version: "1.15.0"
nested: nested:
@@ -232,7 +223,7 @@ packages:
description: description:
name: nested name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
path: path:
@@ -240,7 +231,7 @@ packages:
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider: path_provider:
@@ -248,7 +239,7 @@ packages:
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.5" version: "2.1.5"
path_provider_android: path_provider_android:
@@ -256,7 +247,7 @@ packages:
description: description:
name: path_provider_android name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.17" version: "2.2.17"
path_provider_foundation: path_provider_foundation:
@@ -264,7 +255,7 @@ packages:
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
path_provider_linux: path_provider_linux:
@@ -272,7 +263,7 @@ packages:
description: description:
name: path_provider_linux name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.1" version: "2.2.1"
path_provider_platform_interface: path_provider_platform_interface:
@@ -280,7 +271,7 @@ packages:
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
path_provider_windows: path_provider_windows:
@@ -288,55 +279,63 @@ packages:
description: description:
name: path_provider_windows name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
permission_handler: permission_handler:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "10.4.5" version: "12.0.1"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "10.3.6" version: "13.0.1"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_apple name: permission_handler_apple
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "9.1.4" version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface: permission_handler_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.12.0" version: "4.3.0"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_windows name: permission_handler_windows
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.1.3" version: "0.2.1"
platform: platform:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.6" version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
@@ -344,81 +343,81 @@ packages:
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:
name: provider name: provider
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.1.5" version: "6.1.5+1"
record: record:
dependency: "direct main" dependency: "direct main"
description: description:
name: record name: record
sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817 sha256: "9dbc6ff3e784612f90a9b001373c45ff76b7a08abd2bd9fdf72c242320c8911c"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.0.0" version: "6.1.1"
record_android: record_android:
dependency: transitive dependency: transitive
description: description:
name: record_android name: record_android
sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b" sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.3.3" version: "1.4.1"
record_ios: record_ios:
dependency: transitive dependency: transitive
description: description:
name: record_ios name: record_ios
sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb" sha256: "13e241ed9cbc220534a40ae6b66222e21288db364d96dd66fb762ebd3cb77c71"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.0" version: "1.1.2"
record_linux: record_linux:
dependency: transitive dependency: transitive
description: description:
name: record_linux name: record_linux
sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95" sha256: "235b1f1fb84e810f8149cc0c2c731d7d697f8d1c333b32cb820c449bf7bb72d8"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.1" version: "1.2.1"
record_macos: record_macos:
dependency: transitive dependency: transitive
description: description:
name: record_macos name: record_macos
sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd" sha256: "2849068bb59072f300ad63ed146e543d66afaef8263edba4de4834fc7c8d4d35"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.0" version: "1.1.1"
record_platform_interface: record_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: record_platform_interface name: record_platform_interface
sha256: c1ad38f51e4af88a085b3e792a22c685cb3e7c23fc37aa7ce44c4cf18f25fe89 sha256: b0065fdf1ec28f5a634d676724d388a77e43ce7646fb049949f58c69f3fcb4ed
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
record_web: record_web:
dependency: transitive dependency: transitive
description: description:
name: record_web name: record_web
sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb sha256: "4f0adf20c9ccafcc02d71111fd91fba1ca7b17a7453902593e5a9b25b74a5c56"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.9" version: "1.2.0"
record_windows: record_windows:
dependency: transitive dependency: transitive
description: description:
name: record_windows name: record_windows
sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99" sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.6" version: "1.0.7"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -429,7 +428,7 @@ packages:
description: description:
name: source_span name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
string_scanner: string_scanner:
@@ -437,7 +436,7 @@ packages:
description: description:
name: string_scanner name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized: synchronized:
@@ -445,7 +444,7 @@ packages:
description: description:
name: synchronized name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.3.0+3" version: "3.3.0+3"
term_glyph: term_glyph:
@@ -453,7 +452,7 @@ packages:
description: description:
name: term_glyph name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.2.2" version: "1.2.2"
typed_data: typed_data:
@@ -461,7 +460,7 @@ packages:
description: description:
name: typed_data name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
uuid: uuid:
@@ -469,7 +468,7 @@ packages:
description: description:
name: uuid name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
vector_math: vector_math:
@@ -477,7 +476,7 @@ packages:
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
web: web:
@@ -485,7 +484,7 @@ packages:
description: description:
name: web name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
xdg_directories: xdg_directories:
@@ -493,9 +492,9 @@ packages:
description: description:
name: xdg_directories name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "http://175.24.250.68:4000" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
sdks: sdks:
dart: ">=3.6.2 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.0"

View File

@@ -3,10 +3,11 @@ description: "ai_chat_assistant"
publish_to: 'none' publish_to: 'none'
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ^3.6.2 sdk: ^3.5.0
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
meta: ^1.15.0
fluttertoast: ^8.2.12 fluttertoast: ^8.2.12
record: ^6.0.0 record: ^6.0.0
http: ^1.4.0 http: ^1.4.0
@@ -14,10 +15,12 @@ dependencies:
flutter_markdown: ^0.7.7+1 flutter_markdown: ^0.7.7+1
audioplayers: ^5.2.1 audioplayers: ^5.2.1
uuid: ^3.0.5 uuid: ^3.0.5
permission_handler: ^10.3.0 permission_handler: ^12.0.0
provider: ^6.1.5 provider: ^6.1.5
flutter_tts: ^4.2.0 flutter_tts: ^4.2.0
basic_intl: ^0.2.0 # basic_intl: 0.2.0
basic_intl:
path: packages/basic_intl
# flutter_ingeek_carkey: 1.4.7 # flutter_ingeek_carkey: 1.4.7
# app_car: # app_car:
# path: ../app_car # path: ../app_car
@@ -27,6 +30,11 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/images/ - assets/images/
plugin:
platforms:
android:
package: com.example.ai_assistant_plugin
pluginClass: AiAssistantPlugin
fonts: fonts:
- family: VWHead_Bold - family: VWHead_Bold
fonts: fonts: