xworkmate-app/lib/widgets/assistant_focus_panel_previews.dart
2026-04-08 20:27:35 +08:00

651 lines
21 KiB
Dart

// ignore_for_file: unused_import, unnecessary_import
import 'package:flutter/material.dart';
import '../app/app_controller.dart';
import '../i18n/app_language.dart';
import '../models/app_models.dart';
import '../runtime/runtime_models.dart';
import '../theme/app_palette.dart';
import 'chrome_quick_action_buttons.dart';
import 'settings_focus_quick_actions.dart';
import 'surface_card.dart';
import 'assistant_focus_panel_core.dart';
import 'assistant_focus_panel_support.dart';
class TasksFocusPreviewInternal extends StatelessWidget {
const TasksFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final items = <DerivedTaskItem>[
...typedController.tasksController.running.take(2),
...typedController.tasksController.queue.take(2),
...typedController.tasksController.history.take(1),
].take(4).toList(growable: false);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FocusPillInternal(
label: appText(
'运行中 ${typedController.tasksController.running.length}',
'Running ${typedController.tasksController.running.length}',
),
),
FocusPillInternal(
label: appText(
'队列 ${typedController.tasksController.queue.length}',
'Queue ${typedController.tasksController.queue.length}',
),
),
FocusPillInternal(
label: appText(
'计划 ${typedController.tasksController.scheduled.length}',
'Scheduled ${typedController.tasksController.scheduled.length}',
),
),
],
),
const SizedBox(height: 12),
if (items.isEmpty)
PreviewEmptyStateInternal(
message:
typedController.connection.status ==
RuntimeConnectionStatus.connected
? appText('当前没有任务摘要。', 'No task summary yet.')
: appText(
'连接 Gateway 后这里会显示任务摘要。',
'Connect a gateway to load task summaries.',
),
)
else
...items.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FocusListTileInternal(
title: item.title,
subtitle: item.summary,
trailing: item.status,
),
),
),
],
);
}
}
class SkillsFocusPreviewInternal extends StatelessWidget {
const SkillsFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final items = typedController.isSingleAgentMode
? typedController
.assistantImportedSkillsForSession(
typedController.currentSessionKey,
)
.take(4)
.map(
(skill) => GatewaySkillSummary(
name: skill.label,
description: skill.description,
source: skill.sourcePath,
skillKey: skill.key,
primaryEnv: null,
eligible: true,
disabled: false,
missingBins: const <String>[],
missingEnv: const <String>[],
missingConfig: const <String>[],
),
)
.toList(growable: false)
: typedController.skills.take(4).toList(growable: false);
if (items.isEmpty) {
return PreviewEmptyStateInternal(
message: typedController.isSingleAgentMode
? (typedController.currentSingleAgentNeedsAiGatewayConfiguration
? appText(
'当前没有可用的外部 Agent ACP 端点,请先配置 ACP Server。',
'No external Agent ACP endpoint is available. Configure an ACP server first.',
)
: appText(
'当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。',
'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.',
))
: typedController.connection.status ==
RuntimeConnectionStatus.connected
? appText(
'当前代理没有已加载技能。',
'No skills are loaded for the active agent.',
)
: appText(
'连接 Gateway 后可查看技能摘要。',
'Connect a gateway to inspect skills here.',
),
);
}
return Column(
children: items
.map(
(skill) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FocusListTileInternal(
title: skill.name,
subtitle: skill.description,
trailing: skill.disabled
? appText('已禁用', 'Disabled')
: appText('已启用', 'Enabled'),
),
),
)
.toList(growable: false),
);
}
}
class NodesFocusPreviewInternal extends StatelessWidget {
const NodesFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final items = typedController.instances.take(4).toList(growable: false);
if (items.isEmpty) {
return PreviewEmptyStateInternal(
message: appText('当前没有节点可显示。', 'No nodes are available right now.'),
);
}
return Column(
children: items
.map(
(instance) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FocusListTileInternal(
title: instance.host?.trim().isNotEmpty == true
? instance.host!
: instance.id,
subtitle:
[instance.platform, instance.deviceFamily, instance.ip]
.whereType<String>()
.where((item) => item.trim().isNotEmpty)
.join(' · '),
trailing: instance.mode ?? appText('未知', 'Unknown'),
),
),
)
.toList(growable: false),
);
}
}
class AgentsFocusPreviewInternal extends StatelessWidget {
const AgentsFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final items = typedController.agents.take(5).toList(growable: false);
if (items.isEmpty) {
return PreviewEmptyStateInternal(
message: appText('当前没有代理摘要。', 'No agents are available right now.'),
);
}
return Column(
children: items
.map(
(agent) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FocusListTileInternal(
title: '${agent.emoji} ${agent.name}',
subtitle: agent.id,
trailing: agent.name == typedController.activeAgentName
? appText('当前', 'Active')
: agent.theme,
),
),
)
.toList(growable: false),
);
}
}
class McpFocusPreviewInternal extends StatelessWidget {
const McpFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final items = typedController.connectors.take(4).toList(growable: false);
if (items.isEmpty) {
return PreviewEmptyStateInternal(
message: appText(
'当前没有 MCP 连接器。连接 Gateway 后这里会显示工具摘要。',
'No MCP connectors yet. Connect a gateway to load tool summaries here.',
),
);
}
return Column(
children: items
.map(
(connector) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FocusListTileInternal(
title: connector.label,
subtitle: connector.detailLabel,
trailing: connector.status,
),
),
)
.toList(growable: false),
);
}
}
class ClawHubFocusPreviewInternal extends StatelessWidget {
const ClawHubFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final skillCount = typedController.isSingleAgentMode
? typedController.currentAssistantSkillCount
: typedController.skills.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FocusPillInternal(
label: appText('已加载技能 $skillCount', 'Loaded skills $skillCount'),
),
FocusPillInternal(
label: appText(
'关注入口 ${typedController.assistantNavigationDestinations.length}',
'Pinned ${typedController.assistantNavigationDestinations.length}',
),
),
],
),
const SizedBox(height: 12),
PreviewEmptyStateInternal(
message: appText(
'ClawHub 适合放在侧板做快速搜索或安装入口;需要完整终端交互时,再打开全页。',
'Use ClawHub in the side panel for quick access. Open the full page when you need the terminal workflow.',
),
),
],
);
}
}
class SecretsFocusPreviewInternal extends StatelessWidget {
const SecretsFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final items = typedController.secretReferences
.take(4)
.toList(growable: false);
if (items.isEmpty) {
return PreviewEmptyStateInternal(
message: appText(
'当前没有密钥引用摘要。',
'No masked secret references are available yet.',
),
);
}
return Column(
children: items
.map(
(secret) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FocusListTileInternal(
title: secret.name,
subtitle: '${secret.provider} · ${secret.module}',
trailing: secret.status,
),
),
)
.toList(growable: false),
);
}
}
class AiGatewayFocusPreviewInternal extends StatelessWidget {
const AiGatewayFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final items = typedController.models.take(4).toList(growable: false);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FocusPillInternal(label: typedController.connection.status.label),
FocusPillInternal(
label: appText(
'模型 ${typedController.models.length}',
'Models ${typedController.models.length}',
),
),
],
),
const SizedBox(height: 12),
if (items.isEmpty)
PreviewEmptyStateInternal(
message: appText(
'当前没有 LLM API 模型摘要。',
'No LLM API model summary is available yet.',
),
)
else
...items.map(
(model) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FocusListTileInternal(
title: model.name,
subtitle: model.provider,
trailing: model.id,
),
),
),
],
);
}
}
class SettingsFocusPreviewInternal extends StatelessWidget {
const SettingsFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final themeMode = typedController.themeMode;
final languageLabel = typedController.appLanguage == AppLanguage.zh
? appText('中文', 'Chinese')
: 'English';
final themeLabel = switch (themeMode) {
ThemeMode.dark => appText('深色', 'Dark'),
ThemeMode.light => appText('浅色', 'Light'),
ThemeMode.system => appText('跟随系统', 'System'),
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsFocusQuickActions(
appLanguage: typedController.appLanguage,
themeMode: themeMode,
onToggleLanguage: typedController.toggleAppLanguage,
onToggleTheme: () {
typedController.setThemeMode(
themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark,
);
},
languageButtonKey: const Key(
'assistant-focus-settings-language-toggle',
),
themeButtonKey: const Key('assistant-focus-settings-theme-toggle'),
),
const SizedBox(height: 12),
FocusListTileInternal(
title: appText('语言', 'Language'),
subtitle: appText('当前界面语言', 'Current interface language'),
trailing: languageLabel,
),
const SizedBox(height: 8),
FocusListTileInternal(
title: appText('主题', 'Theme'),
subtitle: appText('当前显示模式', 'Current display mode'),
trailing: themeLabel,
),
const SizedBox(height: 8),
FocusListTileInternal(
title: appText('执行目标', 'Execution target'),
subtitle: appText(
'Assistant 默认运行位置',
'Default assistant execution target',
),
trailing: typedController.assistantExecutionTarget.label,
),
const SizedBox(height: 8),
FocusListTileInternal(
title: appText('权限', 'Permissions'),
subtitle: appText(
'Assistant 默认权限级别',
'Default assistant permission level',
),
trailing: typedController.assistantPermissionLevel.label,
),
],
);
}
}
class LanguageFocusPreviewInternal extends StatelessWidget {
const LanguageFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final currentLabel = typedController.appLanguage == AppLanguage.zh
? appText('中文', 'Chinese')
: 'English';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ChromeLanguageActionButton(
key: const Key('assistant-focus-language-toggle'),
appLanguage: typedController.appLanguage,
compact: false,
tooltip: appText('切换语言', 'Toggle language'),
onPressed: typedController.toggleAppLanguage,
),
const SizedBox(height: 12),
FocusListTileInternal(
title: appText('当前语言', 'Current language'),
subtitle: appText(
'点击上方按钮即可在中英文界面之间切换。',
'Use the button above to switch between Chinese and English.',
),
trailing: currentLabel,
),
],
);
}
}
class ThemeFocusPreviewInternal extends StatelessWidget {
const ThemeFocusPreviewInternal({super.key, required this.controller});
final AssistantFocusControllerInternal controller;
@override
Widget build(BuildContext context) {
final typedController = castAssistantFocusControllerInternal(controller);
final themeMode = typedController.themeMode;
final themeLabel = switch (themeMode) {
ThemeMode.dark => appText('深色', 'Dark'),
ThemeMode.light => appText('浅色', 'Light'),
ThemeMode.system => appText('跟随系统', 'System'),
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ChromeIconActionButton(
key: const Key('assistant-focus-theme-toggle'),
icon: chromeThemeToggleIcon(themeMode),
tooltip: chromeThemeToggleTooltip(themeMode),
onPressed: () {
typedController.setThemeMode(
themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark,
);
},
),
const SizedBox(height: 12),
FocusListTileInternal(
title: appText('当前主题', 'Current theme'),
subtitle: appText(
'点击上方按钮即可切换亮度模式。',
'Use the button above to switch appearance mode.',
),
trailing: themeLabel,
),
],
);
}
}
class FocusListTileInternal extends StatelessWidget {
const FocusListTileInternal({
super.key,
required this.title,
required this.subtitle,
required this.trailing,
});
final String title;
final String subtitle;
final String trailing;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: palette.strokeSoft),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
subtitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textSecondary,
height: 1.3,
),
),
const SizedBox(height: 8),
Text(
trailing,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelLarge?.copyWith(
color: palette.textPrimary,
),
),
],
),
);
}
}
class FocusPillInternal extends StatelessWidget {
const FocusPillInternal({super.key, required this.label});
final String label;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: palette.strokeSoft),
),
child: Text(
label,
style: theme.textTheme.labelLarge?.copyWith(
color: palette.textSecondary,
),
),
);
}
}
class PreviewEmptyStateInternal extends StatelessWidget {
const PreviewEmptyStateInternal({super.key, required this.message});
final String message;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: palette.strokeSoft),
),
child: Text(
message,
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textSecondary,
height: 1.35,
),
),
);
}
}