feat: 应用名称更新为灵可析并增强 Chat 功能

- 更新 Android/iOS 应用名称和图标为灵可析
- Chat 支持取消正在运行的 Agent 对话
- 改进 ChatBloc 状态管理(区分发送/等待/流式/取消状态)
- HomeScreen 支持外部注入 ChatBloc 和显示等待指示器
- 后端 Agent 运行服务优化(消息处理、usage 追踪)
- 补充相关单元测试和 Widget 测试
This commit is contained in:
qzl
2026-03-10 18:39:53 +08:00
parent b48f7abf72
commit 487405aa5b
50 changed files with 768 additions and 284 deletions
@@ -31,6 +31,8 @@ const _rippleDurationMs = 1200;
const _recordingDotSize = 10.0;
const _transcribingSpinnerSize = 18.0;
const _transcribingStrokeWidth = 2.0;
const _inputActionButtonKey = ValueKey('home_input_action_button');
const _inputActionIconKey = ValueKey('home_input_action_icon');
/// 颜色常量
const _chatBgColor = Color(0xFFF8FAFC);
@@ -40,6 +42,7 @@ class HomeScreen extends StatefulWidget {
final VoiceRecorder? voiceRecorder;
final Future<String> Function(String filePath)? onTranscribeAudio;
final Future<void> Function(String transcript)? onAutoSendTranscript;
final ChatBloc? chatBloc;
final bool autoLoadHistory;
const HomeScreen({
@@ -47,6 +50,7 @@ class HomeScreen extends StatefulWidget {
this.voiceRecorder,
this.onTranscribeAudio,
this.onAutoSendTranscript,
this.chatBloc,
this.autoLoadHistory = true,
});
@@ -72,7 +76,7 @@ class _HomeScreenState extends State<HomeScreen>
void initState() {
super.initState();
_messageController.addListener(_onMessageChanged);
_chatBloc = ChatBloc();
_chatBloc = widget.chatBloc ?? ChatBloc();
_voiceRecorder = widget.voiceRecorder ?? RecordVoiceRecorder();
_transcribeAudio =
widget.onTranscribeAudio ?? _chatBloc.transcribeAudioFile;
@@ -93,7 +97,9 @@ class _HomeScreenState extends State<HomeScreen>
_scrollController.dispose();
_listeningAnimationController.dispose();
_voiceRecorder.dispose();
_chatBloc.close();
if (widget.chatBloc == null) {
_chatBloc.close();
}
RouteNavigationTool.instance.clearNavigator();
super.dispose();
}
@@ -131,7 +137,7 @@ class _HomeScreenState extends State<HomeScreen>
children: [
_buildHeader(context),
Expanded(child: _buildChatArea(context, state)),
_buildInputContainer(context),
_buildInputContainer(context, state),
],
),
),
@@ -185,49 +191,100 @@ class _HomeScreenState extends State<HomeScreen>
}
Widget _buildChatArea(BuildContext context, ChatState state) {
if (state.isLoading && state.items.isEmpty) {
final showWaitingIndicator =
state.isWaitingFirstToken || state.isStreaming || state.isCancelling;
if (state.isLoadingHistory && state.items.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (state.items.isEmpty) {
return const Center(
child: Text(
'开始对话吧',
style: TextStyle(fontSize: 16, color: AppColors.slate400),
),
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Expanded(
child: Center(
child: Text(
'开始对话吧',
style: TextStyle(fontSize: 16, color: AppColors.slate400),
),
),
),
if (showWaitingIndicator) _buildWaitingIndicator(),
],
);
}
return RefreshIndicator(
onRefresh: () => _onRefresh(context),
child: ListView.builder(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(_defaultPadding),
itemCount: state.items.length + (state.hasEarlierHistory ? 1 : 0),
itemBuilder: (context, index) {
if (index == 0 && state.hasEarlierHistory) {
return _buildLoadMoreButton(context, state.isLoading);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: RefreshIndicator(
onRefresh: () => _onRefresh(context),
child: ListView.builder(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(_defaultPadding),
itemCount: state.items.length + (state.hasEarlierHistory ? 1 : 0),
itemBuilder: (context, index) {
if (index == 0 && state.hasEarlierHistory) {
return _buildLoadMoreButton(context, state.isLoadingHistory);
}
final itemIndex = state.hasEarlierHistory ? index - 1 : index;
final item = state.items[itemIndex];
final itemIndex = state.hasEarlierHistory ? index - 1 : index;
final item = state.items[itemIndex];
final showDateDivider =
itemIndex == 0 ||
!_isSameDay(state.items[itemIndex - 1].timestamp, item.timestamp);
final showDateDivider =
itemIndex == 0 ||
!_isSameDay(
state.items[itemIndex - 1].timestamp,
item.timestamp,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (showDateDivider) _buildDateDivider(item.timestamp),
Padding(
padding: const EdgeInsets.only(bottom: _itemSpacing),
child: _buildChatItem(item),
),
],
);
},
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (showDateDivider) _buildDateDivider(item.timestamp),
Padding(
padding: const EdgeInsets.only(bottom: _itemSpacing),
child: _buildChatItem(item),
),
],
);
},
),
),
),
if (showWaitingIndicator) _buildWaitingIndicator(),
],
);
}
Widget _buildWaitingIndicator() {
return Padding(
padding: const EdgeInsets.fromLTRB(
_defaultPadding,
0,
_defaultPadding,
_defaultPadding,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
SizedBox(
width: _transcribingSpinnerSize,
height: _transcribingSpinnerSize,
child: CircularProgressIndicator(
strokeWidth: _transcribingStrokeWidth,
color: AppColors.blue600,
),
),
SizedBox(width: 8),
Text(
'正在思考...',
style: TextStyle(fontSize: 14, color: AppColors.slate500),
),
],
),
);
}
@@ -406,7 +463,9 @@ class _HomeScreenState extends State<HomeScreen>
return UiSchemaRenderer.render(item.uiCard);
}
Widget _buildInputContainer(BuildContext context) {
Widget _buildInputContainer(BuildContext context, ChatState state) {
final isWaitingAgent =
state.isWaitingFirstToken || state.isStreaming || state.isCancelling;
return Container(
padding: const EdgeInsets.all(_inputPadding),
color: _chatBgColor,
@@ -471,10 +530,13 @@ class _HomeScreenState extends State<HomeScreen>
),
const SizedBox(width: 8),
GestureDetector(
key: _inputActionButtonKey,
onTap: _isTranscribing
? null
: _isRecording
? () => _stopRecording(autoSendAfterTranscribe: true)
: isWaitingAgent
? () => _onStopGenerating(context)
: _hasMessage
? () => _sendMessage(context)
: _startRecording,
@@ -488,11 +550,14 @@ class _HomeScreenState extends State<HomeScreen>
),
)
: Icon(
_isRecording || _hasMessage
key: _inputActionIconKey,
_isRecording || isWaitingAgent
? LucideIcons.square
: _hasMessage
? LucideIcons.send
: LucideIcons.mic,
size: _iconSize,
color: _isRecording || _hasMessage
color: _isRecording || isWaitingAgent || _hasMessage
? AppColors.blue600
: AppColors.slate500,
),
@@ -511,7 +576,7 @@ class _HomeScreenState extends State<HomeScreen>
if (content.isEmpty) return;
FocusScope.of(context).unfocus();
_messageController.clear();
context.read<ChatBloc>().sendMessage(content);
await context.read<ChatBloc>().sendMessage(content);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
@@ -524,6 +589,16 @@ class _HomeScreenState extends State<HomeScreen>
});
}
Future<void> _onStopGenerating(BuildContext context) async {
final canceled = await context.read<ChatBloc>().cancelCurrentRun();
if (!mounted) {
return;
}
if (canceled) {
Toast.show(context, '已停止等待回复', type: ToastType.info);
}
}
Widget _buildListeningIndicator() {
return SizedBox(
height: _inputMinHeight,