Compare commits

...

11 Commits

Author SHA1 Message Date
guangfei.zhao
87cc15dd53 UPDATE README 2025-09-23 10:58:47 +08:00
guangfei.zhao
6523f8e512 feat: 改进聊天弹窗和遮罩层交互,优化状态管理和绘制逻辑 2025-09-23 09:53:49 +08:00
guangfei.zhao
b9ab384fde 使用Overlay来弹出聊天窗口 2025-09-22 17:51:21 +08:00
guangfei.zhao
049cb7b5c4 新增一个chat_popup 来显示聊天窗口 2025-09-22 17:15:44 +08:00
guangfei.zhao
e04110c6d5 feat:plugin编译为aar包引用 2025-09-22 11:12:45 +08:00
guangfei.zhao
0509096925 使用 assets_util 来管理 assets 2025-09-19 11:40:38 +08:00
guangfei.zhao
b84899ece3 feat: 修复了插件需要外部Provider的问题;加上了一个简易的车控状态返回 2025-09-18 15:53:39 +08:00
guangfei.zhao
922818f39f FloatingIcon 加入拖动的动画,以及自动吸附 2025-09-17 18:07:38 +08:00
guangfei.zhao
33aef48f84 同步 oneApp 的代码 2025-09-17 15:09:05 +08:00
guangfei.zhao
62924296b2 fix: 修复assets在example app中不能使用的问题 & 加入maven local的链接,修复flutter无法打包 aar的问题 2025-09-10 17:33:26 +08:00
guangfei.zhao
4176116bb3 feat: 添加README以及example项目,修复plugin的路径,新建basic_intl的本地依赖 2025-09-10 15:35:07 +08:00
96 changed files with 5408 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/profile
/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更新
- 多层状态协调
- 异步状态同步
---

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 AI Chat Assistant
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

538
README.md
View File

@@ -1,16 +1,534 @@
# 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,
samples, guidance on mobile development, and a full API reference.
```
lib/
├── app.dart # 主应用入口
├── manager.dart # AI 聊天助手管理器
├── ai_chat_assistant.dart # 导出文件
├── bloc/ # 状态管理
│ ├── easy_bloc.dart # 自定义轻量级状态管理框架
│ ├── ai_chat_cubit.dart # AI 聊天命令状态管理
│ └── command_state.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 # 车辆状态信息
├── pages/ # 页面
│ └── full_screen.dart # 全屏聊天页面
├── screens/ # 界面页面
│ ├── main_screen.dart # 主界面
│ └── part_screen.dart # 部分屏聊天界面
├── services/ # 核心服务
│ ├── 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/ # 工具类
│ ├── assets_util.dart # 资源工具
│ ├── common_util.dart # 通用工具
│ ├── tts_engine_manager.dart # TTS 引擎管理
│ └── tts_util.dart # TTS 工具
└── widgets/ # UI 组件
├── chat/ # 聊天相关组件
│ └── chat_window_content.dart # 聊天窗口内容
├── assistant_avatar.dart # AI 助手头像
├── chat_box.dart # 聊天框
├── chat_bubble.dart # 聊天气泡
├── chat_floating_icon.dart # 聊天浮动图标
├── chat_footer.dart # 聊天底部
├── chat_header.dart # 聊天头部
├── chat_popup.dart # 聊天弹出层
├── floating_icon.dart # 浮动图标
├── floating_icon_with_wave.dart # 带波纹的浮动图标
├── gradient_background.dart # 渐变背景
├── large_audio_wave.dart # 大音频波形
├── mini_audio_wave.dart # 小音频波形
├── rotating_image.dart # 旋转图片组件
├── voice_animation.dart # 语音动画
└── _hole_overlay.dart # 洞穴遮罩层
```
### 🎯 核心架构设计
#### 1. 服务层架构 (Services Layer)
- **MessageService**: 核心消息管理服务负责统一管理聊天消息、语音识别、AI 对话流程
- **ChatSseService**: SSE 流式通信服务,处理与 AI 后端的实时对话
- **CommandService**: 车控命令处理服务,提供车辆控制命令的回调机制
- **TTSService**: 语音合成服务,支持中英文语音播报
- **VoiceRecognitionService**: 语音识别服务基于阿里云SDK处理音频转文字
#### 2. 状态管理架构 (State Management)
- **EasyBloc**: 轻量级状态管理框架,支持事件驱动和状态流
- **EasyCubit**: 简化的状态管理,用于单一状态变更
- **AIChatCommandCubit**: 专门处理车控命令的状态管理
- **MessageService**: 基于Provider的消息状态管理
#### 3. UI 层架构 (UI Layer)
- **ChatPopup**: 主要的聊天弹出层组件,集成浮动图标和聊天窗口
- **ChatFloatingIcon**: 可拖拽的浮动图标
- **ChatWindowContent**: 聊天窗口内容组件
- **全屏模式**: 完整的聊天界面,支持历史记录、操作按钮等
#### 4. 管理器模式 (Manager Pattern)
- **AIChatAssistantManager**: 单例管理器,负责整体协调
- **VehicleCommandHandler**: 抽象的车控命令处理器,支持自定义实现
## 🚗 支持的车控命令
| 命令类型 | 中文描述 | 英文描述 |
|---------|---------|---------|
| 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.5.0
- **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 - 文件路径管理
- **Meta**: ^1.15.0 - 元数据注解
### 自定义包
- **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/ai_chat_assistant.dart';
import 'package:provider/provider.dart';
import 'package:permission_handler/permission_handler.dart';
// 实现车控命令处理器
class MyVehicleCommandHandler extends VehicleCommandHandler {
@override
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command) async {
// 处理车控命令的业务逻辑
print('收到车控命令: ${command.type}, 参数: ${command.params}');
// 更新命令状态为执行中
commandCubit?.emit(AIChatCommandState(
commandId: command.commandId,
commandType: command.type,
params: command.params,
status: AIChatCommandStatus.executing,
timestamp: DateTime.now(),
));
// 模拟命令执行
await Future.delayed(const Duration(seconds: 2));
// 更新命令状态为成功
commandCubit?.emit(AIChatCommandState(
commandId: command.commandId,
commandType: command.type,
params: command.params,
status: AIChatCommandStatus.success,
timestamp: DateTime.now(),
result: {'message': '命令执行成功'},
));
// 返回执行结果
return (true, {'message': '命令已执行'});
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 请求麦克风权限
if (!await Permission.microphone.isGranted) {
await Permission.microphone.request();
}
// 初始化 AI Chat Assistant注册车控命令处理器
AIChatAssistantManager.instance.setupCommandHandle(
commandHandler: MyVehicleCommandHandler(),
);
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/chat_popup.dart';
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// 您的现有 UI
YourExistingWidget(),
// AI 助手聊天弹出层
const ChatPopup(),
],
),
);
}
}
```
## 📖 示例项目
项目包含完整的示例应用,位于 `example/` 目录下:
```bash
cd example
flutter run
```
示例展示了:
- 基础集成方法
- 车控命令处理
- 自定义 UI 主题
- 权限管理
## 🎨 UI 组件
### 聊天弹出层 (ChatPopup)
集成式聊天弹出层组件,包含:
- 浮动图标 (ChatFloatingIcon)
- 聊天窗口 (ChatWindowContent)
- 拖拽定位
- 动画效果
### 浮动图标相关组件
- **ChatFloatingIcon**: 专用聊天浮动图标
- **FloatingIcon**: 通用浮动图标
- **FloatingIconWithWave**: 带波纹效果的浮动图标
### 聊天界面组件
- **ChatWindowContent**: 聊天窗口内容组件
- **ChatBubble**: 聊天气泡组件
- **ChatBox**: 聊天输入框
- **ChatHeader**: 聊天头部
- **ChatFooter**: 聊天底部操作栏
- **AssistantAvatar**: AI 助手头像
### 动画组件
- **VoiceAnimation**: 语音动画组件
- **LargeAudioWave**: 大音频波形动画
- **MiniAudioWave**: 小音频波形动画
- **RotatingImage**: 旋转图片组件
- **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 (自动语音识别) 功能基于阿里云SDK
- 原生音频处理
- 系统权限管理
### 阿里云SDK集成
使用阿里云语音识别SDK配置文件在
- `android/libs/nui-release-1.0.0.aar`
- `android/libs/fastjson-1.1.46.android.jar`
### 插件架构
```yaml
plugin:
platforms:
android:
package: com.example.ai_assistant_plugin
pluginClass: AiAssistantPlugin
```
## 📝 API 文档
### AIChatAssistantManager
核心管理器(单例模式):
```dart
class AIChatAssistantManager {
static final instance = AIChatAssistantManager._internal();
// 设置命令处理器
void setupCommandHandle({required VehicleCommandHandler commandHandler});
// 获取命令状态流
Stream<AIChatCommandState> get commandStateStream;
}
```
### VehicleCommandHandler
车控命令处理器抽象类:
```dart
abstract class VehicleCommandHandler {
AIChatCommandCubit? commandCubit;
// 执行车辆控制命令
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command);
}
```
### MessageService
核心消息管理服务:
```dart
class MessageService extends ChangeNotifier {
// 发送消息
Future<void> sendMessage(String text);
// 开始语音识别
Future<void> startVoiceRecognition();
// 停止语音识别
void stopVoiceRecognition();
// 获取聊天历史
List<ChatMessage> get messages;
}
```
### EasyBloc / EasyCubit
轻量级状态管理框架:
```dart
// 基于事件的状态管理
class MyBloc extends EasyBloc<MyEvent, MyState> {
MyBloc(MyState initialState) : super(initialState);
@override
void _mapEventToState(MyEvent event) {
// 处理事件并发射新状态
}
}
// 简化的状态管理
class MyCubit extends EasyCubit<MyState> {
MyCubit(MyState initialState) : super(initialState);
void updateState(MyState newState) {
emit(newState);
}
}
```
## 🐛 调试和故障排除
### 常见问题
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)
- [阿里云语音识别SDK](https://ai.aliyun.com/nls)
## 📋 更新日志
### v1.0.0+1 (2025-09-23)
#### 新增功能
- 🎯 新增轻量级状态管理框架 (EasyBloc/EasyCubit)
- 🔧 新增 AIChatAssistantManager 单例管理器
- 📦 新增 ChatPopup 集成组件
- 🎨 新增多个动画组件 (VoiceAnimation, MiniAudioWave, RotatingImage 等)
- 💾 集成阿里云语音识别SDK
#### 架构改进
- 🏗️ 采用管理器模式替代回调模式
- 🔄 支持面向对象的车控命令处理器
- 📱 优化聊天界面组件结构
- 🎵 改进语音识别和TTS服务
#### 技术栈更新
- 📦 Flutter SDK 升级到 ^3.5.0
- 🔧 添加 Meta 注解支持
- 🌐 保持与 basic_intl 国际化包的兼容

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
**/*.keystore
**/*.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
zipStoreBase=GRADLE_USER_HOME
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;
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;
package com.example.ai_assistant_plugin;
import java.util.Map;
public class MainActivity extends FlutterActivity implements INativeNuiCallback {
private static final String TTS_CHANNEL = "com.example.ai_chat_assistant/tts";
private static final String ASR_CHANNEL = "com.example.ai_chat_assistant/asr";
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
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 APP_KEY = "bXFFc1V65iYbW6EF";
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 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() {
@Override
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
public void configureFlutterEngine(FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), TTS_CHANNEL)
.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);
public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL);
channel.setMethodCallHandler(this);
HandlerThread asrHandlerThread = new HandlerThread("process_thread");
asrHandlerThread.start();
asrHandler = new Handler(asrHandlerThread.getLooper());
}
@Override
protected void onStart() {
Log.i(TAG, "onStart");
super.onStart();
asrInstance.initialize(this, genAsrInitParams(),
asrCallBack = new AsrCallBack(channel);
asrInstance.initialize(asrCallBack, genAsrInitParams(),
Constants.LogLevel.LOG_LEVEL_NONE, false);
}
@Override
protected void onStop() {
Log.i(TAG, "onStop");
super.onStop();
asrInstance.release();
}
@Override
protected void onDestroy() {
super.onDestroy();
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
ttsAudioTrack.stop();
ttsAudioTrack.releaseAudioTrack();
streamInputTtsInstance.stopStreamInputTts();
streamInputTtsInstance.release();
// asrInstance.release();
// if (asrCallBack != null) {
// asrCallBack.release();
// asrCallBack = null;
// }
}
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 onMethodCall(MethodCall call, @NonNull Result result) {
Map<String, Object> args = call.arguments();
switch (call.method) {
case "startTts":
Object isChinese = args.get("isChinese");
if (isChinese == null || isChinese.toString().isBlank()) {
return;
}
}
@Override
public void onStreamInputTtsDataCallback(byte[] data) {
if (data.length > 0) {
ttsAudioTrack.setAudioData(data);
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;
}
}
}, 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;
sendTts(textArg.toString());
break;
case "completeTts":
completeTts();
break;
case "stopTts":
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() {
asrText = "";
asrHandler.post(() -> {
String setParamsString = genAsrParams();
Log.i(TAG, "nui set params " + setParamsString);
@@ -213,105 +150,12 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
private void stopAsr() {
asrHandler.post(() -> {
asrStopping = true;
long ret = asrInstance.stopDialog();
runOnUiThread(() -> asrMethodChannel.invokeMethod("onAsrStop", null));
handler.post(() -> channel.invokeMethod("onAsrStop", null));
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() {
String str = "";
try {
@@ -433,4 +277,51 @@ public class MainActivity extends FlutterActivity implements INativeNuiCallback
Log.i(TAG, "dialog params: " + 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.AudioManager;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant;
package com.example.ai_assistant_plugin;
public interface AudioPlayerCallback {
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 com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.ai_chat_assistant.token.AccessToken;
import com.example.ai_assistant_plugin.token.AccessToken;
import java.io.File;
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;

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token;
package com.example.ai_assistant_plugin.token;
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

View File

@@ -1,4 +1,4 @@
package com.example.ai_chat_assistant.token;
package com.example.ai_assistant_plugin.token;
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;

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'

92
example/README.md Normal file
View File

@@ -0,0 +1,92 @@
# AI Chat Assistant Example
这是 AI Chat Assistant Flutter 插件的示例应用,展示了如何集成和使用 AI 聊天助手功能。
## 🚀 快速开始
### 1. 安装依赖
```bash
cd example
flutter pub get
```
### 2. 运行应用
```bash
flutter run
```
## 📱 示例功能
### 车控命令处理器示例
本示例展示了如何实现自定义的车控命令处理器:
```dart
class VehicleCommandClent extends VehicleCommandHandler {
@override
Future<(bool, Map<String, dynamic>?)> executeCommand(VehicleCommand command) async {
// 在这里实现具体的车控命令执行逻辑
print('执行车控命令: ${command.type}, 参数: ${command.params}');
// 更新命令状态为执行中
commandCubit?.emit(AIChatCommandState(
commandId: command.commandId,
commandType: command.type,
params: command.params,
status: AIChatCommandStatus.executing,
timestamp: DateTime.now(),
));
// 模拟命令执行
await Future.delayed(const Duration(seconds: 2));
// 更新命令状态为成功
commandCubit?.emit(AIChatCommandState(
commandId: command.commandId,
commandType: command.type,
params: command.params,
status: AIChatCommandStatus.success,
timestamp: DateTime.now(),
result: {'message': '命令执行成功'},
));
return (true, {'message': '命令已执行'});
}
}
```
### 初始化示例
```dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 请求麦克风权限
if (!await Permission.microphone.isGranted) {
await Permission.microphone.request();
}
// 初始化 AI Chat Assistant注册车控命令处理器
AIChatAssistantManager.instance.setupCommandHandle(
commandHandler: VehicleCommandClent()
);
runApp(const MyApp());
}
```
## 🎯 示例特性
- ✅ 完整的车控命令处理示例
- ✅ 权限管理示例
- ✅ 状态管理集成
- ✅ AI 聊天界面集成
- ✅ 语音识别功能
- ✅ TTS 语音播报
## 📖 了解更多
- 查看主项目 [README](../README.md) 了解完整的功能特性
- 查看 [API 文档](../README.md#-api-文档) 了解接口使用方法

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 {
namespace = "com.example.ai_chat_assistant"
namespace = "com.example.example"
compileSdk = flutter.compileSdkVersion
ndkVersion = "29.0.13599879"
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
@@ -21,10 +21,10 @@ android {
defaultConfig {
// 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.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 24
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
@@ -37,11 +37,14 @@ android {
signingConfig = signingConfigs.getByName("debug")
}
}
repositories {
flatDir {
dirs("libs")
}
viewBinding {
// isEnabled = viewBindingEnabled
}
dependenciesInfo {
includeInApk = includeInApk
includeInBundle = includeInBundle
}
ndkVersion = "27.0.12077973"
}
flutter {
@@ -49,6 +52,9 @@ flutter {
}
dependencies {
implementation(files("libs/fastjson-1.1.46.android.jar"))
implementation(files("libs/nuisdk-release.aar"))
// Process .aar files in the libs folder.
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.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.intent.action.TTS_SERVICE" />
</manifest>

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="ai_chat_assistant"
android:label="AI助手"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -41,8 +41,5 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.TTS_SERVICE"/>
</intent>
</queries>
</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 {
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()
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)
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,
recognizing,
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';
/// 车辆控制命令类
class VehicleCommand {
final VehicleCommandType type;
final Map<String, dynamic>? params;
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响应解析
factory VehicleCommand.fromString(

View File

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

View File

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

View File

@@ -1,15 +1,23 @@
import 'package:flutter/material.dart';
import 'dart:ui';
import '../widgets/chat_box.dart';
import '../screens/full_screen.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 PartScreen extends StatefulWidget {
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
State<PartScreen> createState() => _PartScreenState();
@@ -50,14 +58,80 @@ class _PartScreenState extends State<PartScreen> {
}
void _openFullScreen() async {
final messageService = context.read<MessageService>();
widget.onHide?.call();
await Navigator.of(context).push(
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
Widget build(BuildContext context) {
return Scaffold(
@@ -67,9 +141,12 @@ class _PartScreenState extends State<PartScreen> {
if (!_isInitialized) {
return const SizedBox.shrink();
}
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 Stack(
children: [
Positioned.fill(
@@ -92,74 +169,80 @@ class _PartScreenState extends State<PartScreen> {
),
),
),
Padding(
padding: EdgeInsets.only(bottom: constraints.maxHeight * 0.22),
child: Align(
alignment: Alignment.bottomCenter,
child: Consumer<MessageService>(
builder: (context, messageService, child) {
final messageCount = messageService.messages.length;
if (messageCount > _lastMessageCount) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
_lastMessageCount = messageCount;
return Container(
width: chatWidth,
constraints: BoxConstraints(
minHeight: minHeight,
maxHeight: maxHeight,
),
child: 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: Image.asset(
'assets/images/open_in_full.png',
width: 24,
height: 24
Positioned(
left: position.left,
right: position.right,
bottom: position.bottom,
child: Consumer<MessageService>(
builder: (context, messageService, child) {
final messageCount = messageService.messages.length;
if (messageCount > _lastMessageCount) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
_lastMessageCount = messageCount;
return 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: 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,
);
}),
),
onPressed: _openFullScreen,
padding: EdgeInsets.zero,
),
Positioned(
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);
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');
// 分句符
RegExp enders = isChinese ? zhEnders : enEnders;
@@ -76,14 +77,8 @@ class ChatSseService {
}
// 只在达到完整句子时调用 TtsUtil.send
for (final s in sentences) {
String ttsStr=CommonUtil.cleanText(s, true)+"\n";
ttsStr = ttsStr.replaceAllMapped(
RegExp(r'(?<!\s)(\d+)(?!\s)'),
(m) => ' ${m.group(1)} ',
);
print("发送数据到TTS: $ttsStr");
String ttsStr=CommonUtil.cleanText(s, true);
// print("发送数据到TTS: $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(
VehicleCommandType type, Map<String, dynamic>? params);
/// 命令处理器类 - 负责处理来自AI的车辆控制命令
/// 命令处理器类 - 负责处理来自AI的车辆控制命令(使用回调的方式交给App去实现具体的车控逻辑)
class CommandService {
/// 保存主应用注册的回调函数
static CommandCallback? onCommandReceived;

View File

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

View File

@@ -1,5 +1,6 @@
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/tts_util.dart';
import 'package:basic_intl/intl.dart';
@@ -7,11 +8,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.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/classification_service.dart';
import '../services/control_recognition_service.dart';
@@ -20,48 +16,72 @@ import '../services/control_recognition_service.dart';
import 'command_service.dart';
import 'package:fluttertoast/fluttertoast.dart';
// 用单例的模式创建
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;
MessageService._internal() {
_asrChannel.setMethodCallHandler((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;
}
});
// 注册MethodChannel的handler
_asrChannel.setMethodCallHandler(_handleMethodCall);
}
@override
void dispose() {
// 取消注册MethodChannel的handler避免内存泄漏和意外回调
_asrChannel.setMethodCallHandler(null);
_asrCompleter?.completeError('Disposed');
_asrCompleter = null;
super.dispose();
}
// 提供真正的销毁方法(可选,用于应用退出时)
void destroyInstance() {
_asrChannel.setMethodCallHandler(null);
_asrCompleter?.completeError('Destroyed');
_asrCompleter = null;
super.dispose();
_instance = null;
}
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 LocalTtsService _ttsService = LocalTtsService();
// final AudioRecorderService _audioService = AudioRecorderService();
// final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
final TextClassificationService _classificationService =
TextClassificationService();
final TextClassificationService _classificationService = TextClassificationService();
final VehicleCommandService _vehicleCommandService = VehicleCommandService();
final List<ChatMessage> _messages = [];
@@ -106,7 +126,6 @@ class MessageService extends ChangeNotifier {
abortReply();
_latestUserMessageId = null;
_latestAssistantMessageId = null;
_isReplyAborted = false;
changeState(MessageServiceState.recording);
_latestUserMessageId = addMessage("", true, MessageStatus.listening);
_asrChannel.invokeMethod("startAsr");
@@ -123,11 +142,7 @@ class MessageService extends ChangeNotifier {
String addMessage(String text, bool isUser, MessageStatus status) {
String uuid = Uuid().v1();
_messages.add(ChatMessage(
id: uuid,
text: text,
isUser: isUser,
timestamp: DateTime.now(),
status: status));
id: uuid, text: text, isUser: isUser, timestamp: DateTime.now(), status: status));
notifyListeners();
return uuid;
}
@@ -138,6 +153,7 @@ class MessageService extends ChangeNotifier {
return;
}
try {
_isReplyAborted = false;
changeState(MessageServiceState.recognizing);
_asrChannel.invokeMethod("stopAsr");
_asrCompleter = Completer<String>();
@@ -226,8 +242,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted) {
return;
}
final vehicleCommandResponse =
await _vehicleCommandService.getCommandFromText(text);
final vehicleCommandResponse = await _vehicleCommandService.getCommandFromText(text);
if (vehicleCommandResponse == null) {
if (_isReplyAborted) {
return;
@@ -235,10 +250,7 @@ class MessageService extends ChangeNotifier {
String msg = isChinese
? "无法识别车辆控制命令,请重试"
: "Cannot recognize the vehicle control command, please try again";
replaceMessage(
id: _latestAssistantMessageId!,
text: msg,
status: MessageStatus.normal);
replaceMessage(id: _latestAssistantMessageId!, text: msg, status: MessageStatus.normal);
} else {
if (_isReplyAborted) {
return;
@@ -259,8 +271,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted) {
return;
}
if (command.type == VehicleCommandType.unknown ||
command.error.isNotEmpty) {
if (command.type == VehicleCommandType.unknown || command.error.isNotEmpty) {
continue;
}
if (command.type == VehicleCommandType.openAC) {
@@ -268,13 +279,13 @@ class MessageService extends ChangeNotifier {
}
bool isSuccess;
if (containOpenAC && command.type == VehicleCommandType.changeACTemp) {
isSuccess = await Future.delayed(const Duration(milliseconds: 2000),
() => processCommand(command, isChinese));
isSuccess = await Future.delayed(
const Duration(milliseconds: 2000), () => processCommand(command, isChinese));
} else {
isSuccess = await processCommand(command, isChinese);
}
if (isSuccess) {
successCommandList.add(command.type.name);
successCommandList.add(isChinese ? command.type.chinese : command.type.english);
}
}
replaceMessage(
@@ -285,8 +296,7 @@ class MessageService extends ChangeNotifier {
if (_isReplyAborted || successCommandList.isEmpty) {
return;
}
String controlResponse =
await _vehicleCommandService.getControlResponse(successCommandList);
String controlResponse = await _vehicleCommandService.getControlResponse(successCommandList);
if (_isReplyAborted || controlResponse.isEmpty) {
return;
}
@@ -297,20 +307,22 @@ class MessageService extends ChangeNotifier {
Future<bool> processCommand(VehicleCommand command, bool isChinese) async {
String msg = "";
MessageStatus status = MessageStatus.normal;
final (isSuccess, result) = await CommandService.executeCommand(
command.type,
params: command.params);
var commandObjc = VehicleCommand(type: command.type, params: command.params);
var client = AIChatAssistantManager.instance.commandClient;
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) {
msg = isChinese
? "很抱歉,定位车辆位置失败"
: "Sorry, locate the vehicle unsuccessfully";
msg = isChinese ? "很抱歉,定位车辆位置失败" : "Sorry, locate the vehicle unsuccessfully";
status = MessageStatus.failure;
if (isSuccess && result != null && result.isNotEmpty) {
String address = result['address'];
if (address.isNotEmpty) {
msg = isChinese
? "已为您找到车辆位置:\"$address\""
: "The vehicle location is \"$address\"";
msg = isChinese ? "已为您找到车辆位置:\"$address\"" : "The vehicle location is \"$address\"";
status = MessageStatus.success;
}
}
@@ -386,8 +398,7 @@ class MessageService extends ChangeNotifier {
status: MessageStatus.completed,
);
} else {
replaceMessage(
id: messageId, text: responseText, status: MessageStatus.thinking);
replaceMessage(id: messageId, text: responseText, status: MessageStatus.thinking);
}
} catch (e) {
abortReply();
@@ -401,8 +412,7 @@ class MessageService extends ChangeNotifier {
if (index == -1 || messages[index].status != MessageStatus.thinking) {
return;
}
replaceMessage(
id: _latestAssistantMessageId!, status: MessageStatus.aborted);
replaceMessage(id: _latestAssistantMessageId!, status: MessageStatus.aborted);
}
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 {
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,
[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 '../utils/assets_util.dart';
class AssistantAvatar extends StatelessWidget {
final double width;
@@ -15,8 +16,8 @@ class AssistantAvatar extends StatelessWidget {
return SizedBox(
width: width,
height: height,
child: Image.asset(
'assets/images/avatar.png',
child: AssetsUtil.getImageWidget(
'avatar.png',
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 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import '../utils/assets_util.dart';
class ChatBubble extends StatefulWidget {
final ChatMessage message;
@@ -108,7 +109,7 @@ class _ChatBubbleState extends State<ChatBubble> {
case MessageStatus.listening:
case MessageStatus.recognizing:
case MessageStatus.thinking:
icon = RotatingImage(imagePath: 'assets/images/thinking_circle.png');
icon = RotatingImage(imagePath: 'thinking_circle.png');
color = Colors.white;
break;
case MessageStatus.executing:
@@ -123,7 +124,11 @@ class _ChatBubbleState extends State<ChatBubble> {
break;
case MessageStatus.completed:
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;
break;
case MessageStatus.failure:
@@ -301,8 +306,9 @@ class _ChatBubbleState extends State<ChatBubble> {
},
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: Image.asset('assets/images/copy.png',
width: 22, height: 22),
child: AssetsUtil.getImageWidget('copy.png',
width: 22,
height: 22),
),
),
InkWell(
@@ -315,10 +321,12 @@ class _ChatBubbleState extends State<ChatBubble> {
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: _liked
? Image.asset('assets/images/liked2.png',
width: 22, height: 22)
: Image.asset('assets/images/liked1.png',
width: 22, height: 22),
? AssetsUtil.getImageWidget('liked2.png',
width: 22,
height: 22)
: AssetsUtil.getImageWidget('liked1.png',
width: 22,
height: 22),
),
),
InkWell(
@@ -331,10 +339,12 @@ class _ChatBubbleState extends State<ChatBubble> {
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: _disliked
? Image.asset('assets/images/disliked2.png',
width: 22, height: 22)
: Image.asset('assets/images/disliked1.png',
width: 22, height: 22)),
? AssetsUtil.getImageWidget('disliked2.png',
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 '../utils/common_util.dart';
import 'large_audio_wave.dart';
import '../utils/assets_util.dart';
class ChatFooter extends StatefulWidget {
final VoidCallback? onClear;
@@ -48,8 +49,8 @@ class _ChatFooterState extends State<ChatFooter>
padding: const EdgeInsets.only(left: 16, bottom: 12),
child: GestureDetector(
onTap: widget.onClear,
child: Image.asset(
'assets/images/delete.png',
child: AssetsUtil.getImageWidget(
'delete.png',
width: 24,
height: 24,
),
@@ -135,9 +136,11 @@ class _ChatFooterState extends State<ChatFooter>
Padding(
padding: const EdgeInsets.only(bottom: 12, right: 16),
child: GestureDetector(
onTap: () {},
child: Image.asset(
'assets/images/keyboard_mini.png',
onTap: () {
},
child: AssetsUtil.getImageWidget(
'keyboard_mini.png',
width: 24,
height: 24,
),

View File

@@ -44,7 +44,7 @@ class ChatHeader extends StatelessWidget {
? '您的专属看、选、买、用车助手'
: 'Your exclusive assistant',
style: TextStyle(
fontSize: 12,
fontSize: 11,
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:provider/provider.dart';
import '../services/message_service.dart';
import '../screens/full_screen.dart';
import '../pages/full_screen.dart';
import '../screens/part_screen.dart';
import 'floating_icon_with_wave.dart';
import 'dart:async'; // 添加此行
import 'dart:async';
import 'package:basic_intl/intl.dart';
import '../utils/assets_util.dart';
class FloatingIcon extends StatefulWidget {
const FloatingIcon({super.key});
@@ -14,26 +15,35 @@ class FloatingIcon extends StatefulWidget {
State<FloatingIcon> createState() => _FloatingIconState();
}
class _FloatingIconState extends State<FloatingIcon>
with SingleTickerProviderStateMixin {
class _FloatingIconState extends State<FloatingIcon> with TickerProviderStateMixin {
Offset _position = const Offset(10, 120);
final iconSize = 80.0;
bool _isShowPartScreen = false;
bool _isDragging = false;
bool _isAnimating = false;
Offset _targetPosition = const Offset(10, 120);
// 水波纹动画控制器
late AnimationController _waveAnimationController;
// PartScreen 显示动画控制器
late AnimationController _partScreenAnimationController;
// 图片切换定时器
Timer? _imageTimer;
// 添加长按状态标记
bool _isLongPressing = false;
// 新增:图片切换相关
// 图片切换相关
int _imageIndex = 0;
late final List<String> _iconImages = [
'assets/images/ai1_hd.png',
'assets/images/ai0_hd.png',
'ai1_hd.png',
'ai0_hd.png',
];
late final List<String> _iconImagesEn = [
'assets/images/ai1_hd_en.png',
'assets/images/ai0_hd_en.png',
'ai1_hd_en.png',
'ai0_hd_en.png',
];
Timer? _imageTimer; // 用于定时切换图片
@override
void initState() {
@@ -42,18 +52,28 @@ class _FloatingIconState extends State<FloatingIcon>
duration: const Duration(milliseconds: 1000),
vsync: this,
);
// 使用Timer.periodic定时切换图片
// PartScreen动画控制器
_partScreenAnimationController = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
);
// 图片切换定时器
_imageTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
setState(() {
_imageIndex = (_imageIndex + 1) % _iconImages.length;
});
if (mounted && !_isDragging) {
setState(() {
_imageIndex = (_imageIndex + 1) % _iconImages.length;
});
}
});
}
@override
void dispose() {
_waveAnimationController.dispose();
_imageTimer?.cancel(); // 释放Timer
_partScreenAnimationController.dispose();
_imageTimer?.cancel();
super.dispose();
}
@@ -61,67 +81,141 @@ class _FloatingIconState extends State<FloatingIcon>
setState(() {
_isShowPartScreen = true;
});
_partScreenAnimationController.forward();
}
void _hidePartScreen() {
setState(() {
_isShowPartScreen = false;
_partScreenAnimationController.reverse().then((_) {
if (mounted) {
setState(() {
_isShowPartScreen = false;
});
}
});
}
// 显示全屏界面
void _showFullScreen() async {
_hidePartScreen();
final messageService = MessageService.instance;
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const FullScreen(),
builder: (context) => ChangeNotifierProvider.value(
value: messageService, // 传递同一个单例实例
child: const FullScreenPage(),
),
),
);
}
// 更新位置
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);
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);
});
}
// 吸附到屏幕边缘
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
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(
children: [
if (_isShowPartScreen)
PartScreen(
onHide: _hidePartScreen,
FadeTransition(
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(
bottom: _position.dy,
right: _position.dx,
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) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (messageService.isRecording) {
if (!_waveAnimationController.isAnimating) {
_waveAnimationController.repeat();
}
} else {
if (_waveAnimationController.isAnimating) {
_waveAnimationController.stop();
}
}
});
return GestureDetector(
onTap: _showFullScreen,
onLongPress: () async {
debugPrint('Long press');
_isLongPressing = true; // 标记开始长按
_showPartScreen();
_waveAnimationController.repeat();
await messageService.startVoiceInput();
},
onLongPressUp: () async {
debugPrint('Long press up');
_isLongPressing = false; // 清除长按标记
_waveAnimationController.stop();
await messageService.stopAndProcessVoiceInput();
final hasMessage = messageService.messages.isNotEmpty;
@@ -129,23 +223,48 @@ class _FloatingIconState extends State<FloatingIcon>
_hidePartScreen();
}
},
onPanUpdate: (details) => _updatePosition(details.delta),
child: messageService.isRecording
? FloatingIconWithWave(
animationController: _waveAnimationController,
iconSize: iconSize,
waveColor: Colors.white,
)
: Image.asset(
Intl.getCurrentLocale().startsWith('zh')
? _iconImages[_imageIndex]:_iconImagesEn[_imageIndex],
width: iconSize,
height: iconSize,
),
onLongPressEnd: (d) {
debugPrint('Long press end $d');
_isLongPressing = false; // 清除长按标记
},
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: 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 'mini_audio_wave.dart';
@@ -31,8 +32,8 @@ class FloatingIconWithWave extends StatelessWidget {
alignment: Alignment.center,
children: [
// 背景图片
Image.asset(
'assets/images/ai_hd_clean.png',
AssetsUtil.getImageWidget(
'ai_hd_clean.png',
width: iconSize,
height: iconSize,
fit: BoxFit.contain,

View File

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

View File

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