import 'package:flutter/material.dart'; import '../i18n/app_language.dart'; enum AssistantTaskProgressPhase { idle, queued, running, syncingArtifacts, interrupted, stopped, } class AssistantTaskProgressState { const AssistantTaskProgressState({ required this.phase, required this.label, this.value, }); const AssistantTaskProgressState.idle() : phase = AssistantTaskProgressPhase.idle, label = '', value = null; final AssistantTaskProgressPhase phase; final String label; final double? value; bool get visible => phase != AssistantTaskProgressPhase.idle; bool get interrupted => phase == AssistantTaskProgressPhase.interrupted; bool get stopped => phase == AssistantTaskProgressPhase.stopped; bool get recoverable => phase == AssistantTaskProgressPhase.interrupted || phase == AssistantTaskProgressPhase.stopped; bool get running => phase == AssistantTaskProgressPhase.queued || phase == AssistantTaskProgressPhase.running || phase == AssistantTaskProgressPhase.syncingArtifacts; } class AssistantTaskProgressBar extends StatelessWidget { const AssistantTaskProgressBar({ super.key, required this.state, this.onStop, this.onContinue, }); final AssistantTaskProgressState state; final VoidCallback? onStop; final VoidCallback? onContinue; @override Widget build(BuildContext context) { if (!state.visible) { return const SizedBox.shrink(); } final theme = Theme.of(context); final color = state.interrupted ? theme.colorScheme.error : state.stopped ? theme.colorScheme.tertiary : theme.colorScheme.primary; return Container( key: const Key('assistant-task-progress-bar'), constraints: const BoxConstraints(minHeight: 34), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), decoration: BoxDecoration( color: state.interrupted ? theme.colorScheme.errorContainer.withValues(alpha: 0.18) : state.stopped ? theme.colorScheme.tertiaryContainer.withValues(alpha: 0.22) : theme.colorScheme.primaryContainer.withValues(alpha: 0.18), border: Border( top: BorderSide(color: theme.dividerColor.withValues(alpha: 0.42)), bottom: BorderSide(color: theme.dividerColor.withValues(alpha: 0.42)), ), ), child: Row( children: [ Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( state.label, key: const Key('assistant-task-progress-label'), maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.labelSmall?.copyWith( color: color, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 5), LinearProgressIndicator( key: const Key('assistant-task-progress-indicator'), value: state.value, minHeight: 3, color: color, backgroundColor: color.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(999), ), ], ), ), if (state.running && onStop != null) ...[ const SizedBox(width: 8), _AssistantTaskProgressActionButton( key: const Key('assistant-task-progress-stop-button'), icon: Icons.stop_rounded, label: appText('停止', 'Stop'), color: color, onPressed: onStop, ), ], if (state.recoverable && onContinue != null) ...[ const SizedBox(width: 8), _AssistantTaskProgressActionButton( key: const Key('assistant-task-progress-continue-button'), icon: Icons.play_arrow_rounded, label: appText('继续', 'Continue'), color: color, onPressed: onContinue, ), ], ], ), ); } } class _AssistantTaskProgressActionButton extends StatelessWidget { const _AssistantTaskProgressActionButton({ super.key, required this.icon, required this.label, required this.color, required this.onPressed, }); final IconData icon; final String label; final Color color; final VoidCallback? onPressed; @override Widget build(BuildContext context) { return TextButton.icon( onPressed: onPressed, icon: Icon(icon, size: 16), label: Text(label), style: TextButton.styleFrom( foregroundColor: color, minimumSize: const Size(0, 28), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), tapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ), ); } } AssistantTaskProgressState assistantTaskProgressState({ required bool pending, required String lifecycleStatus, required String lastResultCode, required String artifactSyncStatus, }) { final syncStatus = artifactSyncStatus.trim().toLowerCase(); final status = lifecycleStatus.trim().toLowerCase(); final result = lastResultCode.trim().toUpperCase(); if (status == 'queued' || syncStatus == 'queued' || result == 'QUEUED') { return AssistantTaskProgressState( phase: AssistantTaskProgressPhase.queued, label: appText('任务已排队,等待执行...', 'Task queued, waiting to run...'), value: 0.18, ); } if (pending && syncStatus == 'syncing') { return AssistantTaskProgressState( phase: AssistantTaskProgressPhase.syncingArtifacts, label: appText('正在同步生成文件...', 'Syncing generated files...'), value: 0.82, ); } if (pending) { return AssistantTaskProgressState( phase: AssistantTaskProgressPhase.running, label: appText('任务运行中...', 'Task running...'), ); } if (status == 'interrupted' || syncStatus == 'interrupted') { return AssistantTaskProgressState( phase: AssistantTaskProgressPhase.interrupted, label: _interruptedTaskProgressLabel(result), value: 0.48, ); } if (result == 'ABORTED') { return AssistantTaskProgressState( phase: AssistantTaskProgressPhase.stopped, label: appText( '任务已停止,可继续补充需求恢复执行。', 'Task stopped. Continue by adding the next request.', ), value: 0, ); } return const AssistantTaskProgressState.idle(); } String _interruptedTaskProgressLabel(String result) { if (result == 'ACP_HTTP_HANDSHAKE_INTERRUPTED') { return appText( 'Bridge 握手中断,本轮请求未完成。', 'Bridge handshake interrupted; this request did not complete.', ); } return appText( 'Bridge 响应中断,本轮结果未完成。', 'Bridge response interrupted; this result did not complete.', ); }