commit 86d9daf90a24a9c45f89e69635dfda00208e8ef2 Author: Ding, Shuo Date: Wed Jun 18 11:28:37 2025 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/.gitignore @@ -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 +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# 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 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..9c60152 --- /dev/null +++ b/.metadata @@ -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: "6fba2447e95c451518584c35e25f5433f14d888c" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 6fba2447e95c451518584c35e25f5433f14d888c + base_revision: 6fba2447e95c451518584c35e25f5433f14d888c + - platform: android + create_revision: 6fba2447e95c451518584c35e25f5433f14d888c + base_revision: 6fba2447e95c451518584c35e25f5433f14d888c + + # 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' diff --git a/README.md b/README.md new file mode 100644 index 0000000..8344389 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# app003 + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -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 diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -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 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..9f11899 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.app003" + compileSdk = flutter.compileSdkVersion + ndkVersion = "29.0.13599879" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.app003" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 24 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..83953f6 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/example/app003/MainActivity.java b/android/app/src/main/java/com/example/app003/MainActivity.java new file mode 100644 index 0000000..d963074 --- /dev/null +++ b/android/app/src/main/java/com/example/app003/MainActivity.java @@ -0,0 +1,6 @@ +package com.example.app003; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..c872ab8 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..b17e8f3 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,25 @@ +allprojects { + repositories { + 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/") } + + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..8bf6f1f --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,5 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true +systemProp.https.proxyHost=127.0.0.1 +systemProp.https.proxyPort=7890 \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6701377 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +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 diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..ab39a10 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,25 @@ +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") diff --git a/assets/images/avatar.png b/assets/images/avatar.png new file mode 100644 index 0000000..f708034 Binary files /dev/null and b/assets/images/avatar.png differ diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..b84622b --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'config/theme.dart'; +import 'screens/chat_screen.dart'; + +class AIChatApp extends StatelessWidget { + const AIChatApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'AI Chat Assistant', + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeMode.system, + home: const ChatScreen(), + ); + } +} \ No newline at end of file diff --git a/lib/config/theme.dart b/lib/config/theme.dart new file mode 100644 index 0000000..50ea204 --- /dev/null +++ b/lib/config/theme.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static final ThemeData lightTheme = ThemeData( + primaryColor: const Color(0xFF6C63FF), + scaffoldBackgroundColor: Colors.white, + fontFamily: 'Roboto', + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF6C63FF), + brightness: Brightness.light, + ), + ); + + static final ThemeData darkTheme = ThemeData( + primaryColor: const Color(0xFF6C63FF), + scaffoldBackgroundColor: const Color(0xFF121212), + fontFamily: 'Roboto', + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF6C63FF), + brightness: Brightness.dark, + ), + ); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..6964b0c --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; +import 'app.dart'; + +void main() { + runApp(const AIChatApp()); +} diff --git a/lib/models/chat_message.dart b/lib/models/chat_message.dart new file mode 100644 index 0000000..751ab39 --- /dev/null +++ b/lib/models/chat_message.dart @@ -0,0 +1,11 @@ +class ChatMessage { + final String text; + final bool isUser; + final DateTime timestamp; + + ChatMessage({ + required this.text, + required this.isUser, + required this.timestamp, + }); +} \ No newline at end of file diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart new file mode 100644 index 0000000..f31e589 --- /dev/null +++ b/lib/screens/chat_screen.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import '../widgets/ai_avatar.dart'; +import '../widgets/chat_bubble.dart'; +import '../widgets/chat_input.dart'; +import '../widgets/gradient_background.dart'; +import '../models/chat_message.dart'; +import 'dart:convert'; + +class ChatScreen extends StatefulWidget { + const ChatScreen({super.key}); + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + final List _messages = []; + int? _currentStreamingMessageIndex; + int _lastUserMessageCount = 0; // 添加这个变量跟踪用户消息数量 + + void _handleSendMessage(String text) { + if (text.trim().isEmpty) return; + setState(() { + _messages.add(ChatMessage( + text: text, + isUser: true, + timestamp: DateTime.now(), + )); + _lastUserMessageCount = _getUserMessageCount(); // 更新用户消息计数 + _currentStreamingMessageIndex = null; // 重置流索引,强制创建新气泡 + }); + } + + // 处理语音识别后的文本 + void _handleAIResponse(String text) { + setState(() { + _messages.add(ChatMessage( + text: text, + isUser: true, // 用户的消息 + timestamp: DateTime.now(), + )); + _lastUserMessageCount = _getUserMessageCount(); // 更新用户消息计数 + _currentStreamingMessageIndex = null; // 重置流索引,强制创建新气泡 + }); + } + + // 计算用户消息总数 + int _getUserMessageCount() { + return _messages.where((msg) => msg.isUser).length; + } + + // 处理 AI 流式响应 + void _handleAIStreamResponse(String text, bool isComplete) { + // 检查是否有新的用户消息,如果有,应该创建新的AI响应 + final currentUserMessageCount = _getUserMessageCount(); + final hasNewUserMessage = currentUserMessageCount > _lastUserMessageCount; + + setState(() { + if (hasNewUserMessage || _currentStreamingMessageIndex == null) { + // 有新用户消息或首次响应,创建新气泡 + _messages.add(ChatMessage( + text: text, + isUser: false, + timestamp: DateTime.now(), + )); + _currentStreamingMessageIndex = _messages.length - 1; + _lastUserMessageCount = currentUserMessageCount; // 更新追踪 + } else { + // 更新现有气泡 + if (_currentStreamingMessageIndex! < _messages.length) { + _messages[_currentStreamingMessageIndex!] = ChatMessage( + text: text, + isUser: false, + timestamp: _messages[_currentStreamingMessageIndex!].timestamp, + ); + } + } + + // 如果完成了,重置索引 + if (isComplete) { + _currentStreamingMessageIndex = null; + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GradientBackground( + child: SafeArea( + child: Column( + children: [ + // Header with AI avatar + SizedBox( + height: 140, + child: Stack( + children: [ + // 背景渐变已由外层GradientBackground提供 + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // AI贴图 + const Padding( + padding: EdgeInsets.only(left: 16, top: 0), + child: AIAvatar(width: 120, height: 140), + ), + const SizedBox(width: 8), + // 文字整体下移 + Padding( + padding: const EdgeInsets.only(top: 48), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Hi, 我是众众!', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Text( + '您的专属看、选、买、用车助手', + style: TextStyle( + fontSize: 14, + color: Colors.white.withOpacity(0.8), + ), + ), + ], + ), + ), + // 可选:右上角关闭按钮 + const Spacer(), + Padding( + padding: const EdgeInsets.only(top: 16, right: 16), + child: Icon(Icons.close, color: Colors.white), + ), + ], + ), + // 底部分割线 + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: 1, + color: Colors.white.withOpacity(0.15), + ), + ), + ], + ), + ), + + // Chat messages + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _messages.length, + reverse: true, + itemBuilder: (context, index) { + final message = _messages[_messages.length - 1 - index]; + return ChatBubble(message: message); + }, + ), + ), + + // Input area + ChatInput( + onSendMessage: _handleSendMessage, + onAIResponse: _handleAIResponse, + onAIStreamResponse: _handleAIStreamResponse, // 添加新回调 + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/ai_avatar.dart b/lib/widgets/ai_avatar.dart new file mode 100644 index 0000000..a2c0b8f --- /dev/null +++ b/lib/widgets/ai_avatar.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class AIAvatar extends StatelessWidget { + final double width; + final double height; + + const AIAvatar({ + super.key, + this.width = 120, + this.height = 140, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + height: height, + child: Image.asset( + 'assets/images/avatar.png', + fit: BoxFit.contain, + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/chat_bubble.dart b/lib/widgets/chat_bubble.dart new file mode 100644 index 0000000..8a1ef08 --- /dev/null +++ b/lib/widgets/chat_bubble.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import '../models/chat_message.dart'; + +class ChatBubble extends StatelessWidget { + final ChatMessage message; + + const ChatBubble({super.key, required this.message}); + + @override + Widget build(BuildContext context) { + return Align( + alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: message.isUser + ? const Color(0xFF6C63FF) + : Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + message.text, + style: TextStyle( + color: message.isUser ? Colors.white : Colors.white, + fontSize: 16, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/chat_input.dart b/lib/widgets/chat_input.dart new file mode 100644 index 0000000..ad6f0f4 --- /dev/null +++ b/lib/widgets/chat_input.dart @@ -0,0 +1,351 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:record/record.dart'; +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; +import 'dart:typed_data'; +import 'dart:convert'; +import 'package:path_provider/path_provider.dart'; +import 'dart:math'; + +class ChatInput extends StatefulWidget { + final Function(String) onSendMessage; + final Function(String) onAIResponse; + // 新增一个回调,用于处理 AI 流式回复 + final Function(String, bool) onAIStreamResponse; + + const ChatInput({ + super.key, + required this.onSendMessage, + required this.onAIResponse, + required this.onAIStreamResponse, // 添加这个参数 + }); + + @override + State createState() => _ChatInputState(); +} + +class _ChatInputState extends State { + final TextEditingController _controller = TextEditingController(); + final AudioRecorder _recorder = AudioRecorder(); + List _audioBuffer = []; + String? _tempFilePath; + bool _isRecording = false; + + // 添加静态变量存储用户ID和会话ID + static String? _cachedUserId; + static String? _cachedConversationId; + + @override + void initState() { + super.initState(); + _checkPermission(); + } + + Future _checkPermission() async { + final hasPermission = await _recorder.hasPermission(); + print('麦克风权限状态: $hasPermission'); + } + + @override + void dispose() { + _controller.dispose(); + _recorder.dispose(); + super.dispose(); + } + + void _handleSubmit() { + final text = _controller.text; + if (text.trim().isNotEmpty) { + widget.onSendMessage(text); + _controller.clear(); + } + } + + Future _startRecording() async { + setState(() { + _isRecording = true; + _audioBuffer = []; + }); + + print('开始录音...'); + + try { + // 使用文件录音可能更稳定 + final tempDir = await getTemporaryDirectory(); + _tempFilePath = '${tempDir.path}/temp_audio_${DateTime.now().millisecondsSinceEpoch}.opus'; + + print('录音文件路径: $_tempFilePath'); + + if (await _recorder.hasPermission()) { + + await _recorder.start( + RecordConfig(encoder: AudioEncoder.opus), + path: _tempFilePath!, + ); + + print('录音已开始,使用OPUS格式,文件: $_tempFilePath'); + } else { + print('没有麦克风权限,无法开始录音'); + } + } catch (e) { + print('录音开始出错: $e'); + setState(() { + _isRecording = false; + }); + } + } + + Future _stopAndSendRecording() async { + if (!_isRecording) return; + + setState(() { + _isRecording = false; + }); + + print('停止录音...'); + try { + final path = await _recorder.stop(); + print('录音已停止,文件路径: $path'); + + if (path != null) { + final file = File(path); + if (await file.exists()) { + final bytes = await file.readAsBytes(); + print('读取到录音数据: ${bytes.length} 字节'); + + if (bytes.isNotEmpty) { + // 第一步: 发送音频到语音识别接口 + final recognizedText = await _sendAudioToServer(bytes); + if (recognizedText != null) { + // 把识别的文本作为用户消息展示 + widget.onAIResponse(recognizedText); + + // 第二步: 将识别的文本发送到 Chat SSE 接口 + _sendTextToChatSSE(recognizedText); + } + } else { + print('录音数据为空'); + } + } else { + print('录音文件不存在: $path'); + } + } else { + print('录音路径为空'); + } + } catch (e) { + print('停止录音或发送过程出错: $e'); + } + } + + // 修改返回类型,以便获取识别的文本 + Future _sendAudioToServer(List audioBytes) async { + try { + print('准备发送OPUS音频数据,大小: ${audioBytes.length} 字节'); + final uri = Uri.parse('http://143.64.185.20:18606/voice'); + print('发送到: $uri'); + + final request = http.MultipartRequest('POST', uri); + request.files.add( + http.MultipartFile.fromBytes( + 'audio', + audioBytes, + filename: 'record.wav', + contentType: MediaType('audio', 'wav'), + ), + ); + request.fields['lang'] = 'cn'; + + print('发送请求...'); + final streamResponse = await request.send(); + print('收到响应,状态码: ${streamResponse.statusCode}'); + + final response = await http.Response.fromStream(streamResponse); + + if (response.statusCode == 200) { + print('响应内容: ${response.body}'); + final text = _parseTextFromJson(response.body); + if (text != null) { + print('解析出文本: $text'); + return text; // 返回识别的文本 + } else { + print('解析文本失败'); + } + } else { + print('请求失败,状态码: ${response.statusCode},响应: ${response.body}'); + } + } catch (e) { + print('发送录音到服务器时出错: $e'); + } + return null; + } + + // 添加这个方法,用于从JSON响应中解析文本 + String? _parseTextFromJson(String body) { + try { + final decoded = jsonDecode(body); + if (decoded.containsKey('text')) { + return decoded['text'] as String?; + } + return null; + } catch (e) { + print('JSON解析错误: $e, 原始数据: $body'); + return null; + } + } + + // 新增方法:发送文本到 Chat SSE 接口 + void _sendTextToChatSSE(String text) async { + print('将识别的文本发送到 Chat SSE 接口: $text'); + + // 如果用户ID未初始化,则生成一个 + if (_cachedUserId == null) { + _cachedUserId = _generateRandomUserId(6); // 生成6位随机ID + print('初始化用户ID: $_cachedUserId'); + } + + try { + // 使用 HttpClient 来处理 SSE + final client = HttpClient(); + // 设置 URL 和参数 + final chatUri = Uri.parse('http://143.64.185.20:18606/chat'); + final request = await client.postUrl(chatUri); + + // 设置请求头 + request.headers.set('Content-Type', 'application/json'); + request.headers.set('Accept', 'text/event-stream'); + + // 设置请求体 + final body = { + 'message': text, + 'user': _cachedUserId, + }; + + // 如果有缓存的会话ID,则添加到请求体 + if (_cachedConversationId != null) { + body['conversation_id'] = _cachedConversationId; + print('使用缓存的会话ID: $_cachedConversationId'); + } else { + print('首次请求,不使用会话ID'); + } + + request.add(utf8.encode(json.encode(body))); + + // 发送请求并获取 SSE 流 + final response = await request.close(); + + if (response.statusCode == 200) { + print('SSE 连接成功,开始接收流数据'); + + // 创建一个变量保存累积的 AI 回复 + String accumulatedResponse = ''; + + // 使用 transform 将流数据转换为字符串 + await for (final data in response.transform(utf8.decoder)) { + // 处理 SSE 数据(格式为 "data: {...}\n\n") + final lines = data.split('\n'); + + for (var line in lines) { + if (line.startsWith('data:')) { + // 提取 JSON 数据部分 + final jsonStr = line.substring(5).trim(); + + if (jsonStr == '[DONE]') { + // 流结束 + print('SSE 流结束'); + // 发送最终完整的回复,标记为完成 + widget.onAIStreamResponse(accumulatedResponse, true); + break; + } + + try { + final jsonData = json.decode(jsonStr); + + // 尝试提取会话ID (如果存在) + if (jsonData.containsKey('conversation_id') && _cachedConversationId == null) { + _cachedConversationId = jsonData['conversation_id']; + print('从响应中提取并缓存会话ID: $_cachedConversationId'); + } + + // 提取并累加内容片段 + if(jsonData['event'].toString().contains('message')){ + // 代表是有实际消息的数据 + final textChunk = jsonData.containsKey('answer') ? + jsonData['answer'] : ''; + accumulatedResponse += textChunk; + widget.onAIStreamResponse(accumulatedResponse, false); + } + } catch (e) { + print('解析 SSE 数据出错: $e, 原始数据: $jsonStr'); + } + } + } + } + } else { + print('SSE 连接失败,状态码: ${response.statusCode}'); + } + } catch (e) { + print('SSE 连接出错: $e'); + } + } + + // 生成随机用户ID的辅助方法 + String _generateRandomUserId(int length) { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + final random = Random(); + return String.fromCharCodes( + Iterable.generate(length, (_) => chars.codeUnitAt(random.nextInt(chars.length))) + ); + } + + // 添加一个新方法来呼出键盘 + void _showKeyboard() { + FocusScope.of(context).requestFocus(FocusNode()); + // 给输入框焦点,触发键盘弹出 + Future.delayed(const Duration(milliseconds: 50), () { + FocusScope.of(context).requestFocus( + FocusNode()..requestFocus() + ); + }); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8.0), + color: Colors.transparent, + child: Row( + children: [ + Expanded( + child: GestureDetector( + onLongPress: _startRecording, + onLongPressUp: _stopAndSendRecording, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + height: 48, + alignment: Alignment.center, + decoration: BoxDecoration( + color: _isRecording + ? Colors.white.withOpacity(0.25) + : Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(24), + ), + child: Text( + _isRecording ? '正在说话中...' : '按住说话', + style: const TextStyle(color: Colors.white70, fontSize: 16), + ), + ), + ), + ), + IconButton( + // 替换为键盘图标 + icon: const Icon(Icons.keyboard, color: Colors.white), + // 修改点击行为为呼出键盘 + onPressed: _showKeyboard, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/gradient_background.dart b/lib/widgets/gradient_background.dart new file mode 100644 index 0000000..00fbdda --- /dev/null +++ b/lib/widgets/gradient_background.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class GradientBackground extends StatelessWidget { + final Widget child; + + const GradientBackground({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF451663), + Color(0xFF17042B), + Color(0xFF0B021D), + ], + ), + ), + child: child, + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..0dc1dbd --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,434 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.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.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" + url: "https://pub.dev" + source: hosted + version: "8.2.12" + http: + dependency: "direct main" + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + url: "https://pub.dev" + source: hosted + version: "10.0.9" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + record: + dependency: "direct main" + description: + name: record + sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + record_android: + dependency: transitive + description: + name: record_android + sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + record_ios: + dependency: transitive + description: + name: record_ios + sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + record_linux: + dependency: transitive + description: + name: record_linux + sha256: "29e7735b05c1944bb6c9b72a36c08d4a1b24117e712d6a9523c003bde12bf484" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + record_macos: + dependency: transitive + description: + name: record_macos + sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + sha256: "8a575828733d4c3cb5983c914696f40db8667eab3538d4c41c50cbb79e722ef4" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + record_web: + dependency: transitive + description: + name: record_web + sha256: "024c81eb7f51468b1833a3eca8b461c7ca25c04899dba37abe580bb57afd32e4" + url: "https://pub.dev" + source: hosted + version: "1.1.8" + record_windows: + dependency: transitive + description: + name: record_windows + sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.8.1 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..5d66600 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,95 @@ +name: app003 +description: "A new Flutter project." +# 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.8.1 + +# 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 + fluttertoast: ^8.2.4 + record: ^6.0.0 + http: ^1.2.1 + path_provider: ^2.1.2 + +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 + assets: + - assets/images/ + + # 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