This commit is contained in:
2025-06-18 11:28:37 +08:00
commit 86d9daf90a
35 changed files with 1566 additions and 0 deletions

45
.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
.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

30
.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: "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'

16
README.md Normal file
View File

@@ -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.

28
analysis_options.yaml Normal file
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
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

@@ -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 = "../.."
}

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

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="app003"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

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

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,12 @@
<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"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Optional: Add this permission if you want to use bluetooth telephony device like headset/earbuds -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- Optional: Add this permission if you want to save your recordings in public folders -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

25
android/build.gradle.kts Normal file
View File

@@ -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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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")

BIN
assets/images/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

18
lib/app.dart Normal file
View File

@@ -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(),
);
}
}

23
lib/config/theme.dart Normal file
View File

@@ -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,
),
);
}

6
lib/main.dart Normal file
View File

@@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
import 'app.dart';
void main() {
runApp(const AIChatApp());
}

View File

@@ -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,
});
}

View File

@@ -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<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final List<ChatMessage> _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, // 添加新回调
),
],
),
),
),
);
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -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,
),
),
),
);
}
}

351
lib/widgets/chat_input.dart Normal file
View File

@@ -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<ChatInput> createState() => _ChatInputState();
}
class _ChatInputState extends State<ChatInput> {
final TextEditingController _controller = TextEditingController();
final AudioRecorder _recorder = AudioRecorder();
List<int> _audioBuffer = [];
String? _tempFilePath;
bool _isRecording = false;
// 添加静态变量存储用户ID和会话ID
static String? _cachedUserId;
static String? _cachedConversationId;
@override
void initState() {
super.initState();
_checkPermission();
}
Future<void> _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<void> _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<void> _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<String?> _sendAudioToServer(List<int> 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,
),
],
),
);
}
}

View File

@@ -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,
);
}
}

434
pubspec.lock Normal file
View File

@@ -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"

95
pubspec.yaml Normal file
View File

@@ -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