Add global language toggle and app localization
This commit is contained in:
parent
29d339c0ac
commit
51e225ffc0
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
import '../i18n/app_language.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'app_controller.dart';
|
||||
import 'app_metadata.dart';
|
||||
@ -35,6 +37,9 @@ class _XWorkmateAppState extends State<XWorkmateApp> {
|
||||
return MaterialApp(
|
||||
title: kSystemAppName,
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: Locale(_controller.appLanguage.code),
|
||||
supportedLocales: const [Locale('zh'), Locale('en')],
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
themeMode: _controller.themeMode,
|
||||
theme: AppTheme.light(),
|
||||
darkTheme: AppTheme.dark(),
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/gateway_runtime.dart';
|
||||
@ -71,6 +72,7 @@ class AppController extends ChangeNotifier {
|
||||
String get activeAgentName => _agentsController.activeAgentName;
|
||||
String get currentSessionKey => _sessionsController.currentSessionKey;
|
||||
String? get activeRunId => _chatController.activeRunId;
|
||||
AppLanguage get appLanguage => settings.appLanguage;
|
||||
AssistantExecutionTarget get assistantExecutionTarget =>
|
||||
settings.assistantExecutionTarget;
|
||||
AssistantPermissionLevel get assistantPermissionLevel =>
|
||||
@ -122,6 +124,23 @@ class AppController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> toggleAppLanguage() async {
|
||||
await setAppLanguage(
|
||||
settings.appLanguage == AppLanguage.zh ? AppLanguage.en : AppLanguage.zh,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setAppLanguage(AppLanguage language) async {
|
||||
if (settings.appLanguage == language) {
|
||||
return;
|
||||
}
|
||||
setActiveAppLanguage(language);
|
||||
await saveSettings(
|
||||
settings.copyWith(appLanguage: language),
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
}
|
||||
|
||||
void openDetail(DetailPanelData detailPanel) {
|
||||
_detailPanel = detailPanel;
|
||||
notifyListeners();
|
||||
@ -317,6 +336,7 @@ class AppController extends ChangeNotifier {
|
||||
SettingsSnapshot snapshot, {
|
||||
bool refreshAfterSave = true,
|
||||
}) async {
|
||||
setActiveAppLanguage(snapshot.appLanguage);
|
||||
await _settingsController.saveSnapshot(snapshot);
|
||||
_agentsController.restoreSelection(snapshot.gateway.selectedAgentId);
|
||||
if (refreshAfterSave) {
|
||||
@ -363,6 +383,7 @@ class AppController extends ChangeNotifier {
|
||||
Future<void> _initialize() async {
|
||||
try {
|
||||
await _settingsController.initialize();
|
||||
setActiveAppLanguage(settings.appLanguage);
|
||||
await _runtime.initialize();
|
||||
_agentsController.restoreSelection(settings.gateway.selectedAgentId);
|
||||
_sessionsController.configure(
|
||||
|
||||
@ -84,10 +84,10 @@ class AppShell extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: AccountPage(controller: controller),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: AccountPage(controller: controller),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -100,9 +100,6 @@ class AppShell extends StatelessWidget {
|
||||
if (isMobile) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: _AmbientBackground(palette: palette),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -160,16 +157,15 @@ class AppShell extends StatelessWidget {
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: _AmbientBackground(palette: palette),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SidebarNavigation(
|
||||
currentSection: controller.destination,
|
||||
isCollapsed: collapsed,
|
||||
appLanguage: controller.appLanguage,
|
||||
themeMode: controller.themeMode,
|
||||
onSectionChanged: controller.navigateTo,
|
||||
onToggleLanguage: controller.toggleAppLanguage,
|
||||
onToggleCollapsed: controller.toggleSidebar,
|
||||
onOpenAccount: () => controller.navigateTo(
|
||||
WorkspaceDestination.account,
|
||||
@ -183,9 +179,9 @@ class AppShell extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
top: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
),
|
||||
child: AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
@ -193,12 +189,9 @@ class AppShell extends StatelessWidget {
|
||||
padding: EdgeInsets.only(
|
||||
right: showPinnedDetail ? 392 : 0,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: Container(
|
||||
color: palette.canvas.withValues(alpha: 0.16),
|
||||
child: _buildCurrentPage(controller.openDetail),
|
||||
),
|
||||
child: Container(
|
||||
color: palette.canvas,
|
||||
child: _buildCurrentPage(controller.openDetail),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -267,41 +260,3 @@ class AppShell extends StatelessWidget {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _AmbientBackground extends StatelessWidget {
|
||||
const _AmbientBackground({required this.palette});
|
||||
|
||||
final AppPalette palette;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: -120,
|
||||
right: -80,
|
||||
child: Container(
|
||||
width: 340,
|
||||
height: 340,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: palette.accent.withValues(alpha: 0.07),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -120,
|
||||
bottom: -180,
|
||||
child: Container(
|
||||
width: 380,
|
||||
height: 380,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: palette.success.withValues(alpha: 0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,15 +2,14 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../widgets/section_tabs.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../widgets/top_bar.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
const AccountPage({super.key, required this.controller});
|
||||
|
||||
final AppController controller;
|
||||
|
||||
@ -19,7 +18,7 @@ class AccountPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AccountPageState extends State<AccountPage> {
|
||||
String _tab = 'Profile';
|
||||
AccountTab _tab = AccountTab.profile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -33,37 +32,55 @@ class _AccountPageState extends State<AccountPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(title: 'Account', subtitle: '用户身份、工作区切换与登录会话。'),
|
||||
const SizedBox(height: 24),
|
||||
SectionTabs(
|
||||
items: const ['Profile', 'Workspace', 'Sessions'],
|
||||
value: _tab,
|
||||
size: SectionTabsSize.small,
|
||||
onChanged: (value) => setState(() => _tab = value),
|
||||
TopBar(
|
||||
title: appText('账号', 'Account'),
|
||||
subtitle: appText(
|
||||
'用户身份、工作区切换与登录会话。',
|
||||
'Identity, workspace switching, and sign-in sessions.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_tab == 'Profile')
|
||||
SectionTabs(
|
||||
items: AccountTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
size: SectionTabsSize.small,
|
||||
onChanged: (value) => setState(
|
||||
() => _tab = AccountTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_tab == AccountTab.profile)
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
settings.accountUsername.trim().isEmpty
|
||||
? 'Local Operator'
|
||||
? appText('本地操作员', 'Local Operator')
|
||||
: settings.accountUsername,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
settings.accountLocalMode
|
||||
? 'Local mode · Placeholder account session'
|
||||
: 'Unified account entry pending backend integration',
|
||||
? appText(
|
||||
'本地模式 · 占位账号会话',
|
||||
'Local mode · Placeholder account session',
|
||||
)
|
||||
: appText(
|
||||
'统一账号入口等待后端集成',
|
||||
'Unified account entry pending backend integration',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: ValueKey(settings.accountBaseUrl),
|
||||
initialValue: settings.accountBaseUrl,
|
||||
decoration: const InputDecoration(labelText: 'Service URL'),
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('服务地址', 'Service URL'),
|
||||
),
|
||||
onFieldSubmitted: (value) => controller.saveSettings(
|
||||
settings.copyWith(accountBaseUrl: value),
|
||||
),
|
||||
@ -72,7 +89,9 @@ class _AccountPageState extends State<AccountPage> {
|
||||
TextFormField(
|
||||
key: ValueKey(settings.accountUsername),
|
||||
initialValue: settings.accountUsername,
|
||||
decoration: const InputDecoration(labelText: 'Email / Username'),
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('邮箱 / 用户名', 'Email / Username'),
|
||||
),
|
||||
onFieldSubmitted: (value) => controller.saveSettings(
|
||||
settings.copyWith(accountUsername: value),
|
||||
),
|
||||
@ -80,7 +99,7 @@ class _AccountPageState extends State<AccountPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_tab == 'Workspace')
|
||||
if (_tab == AccountTab.workspace)
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -90,12 +109,19 @@ class _AccountPageState extends State<AccountPage> {
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('Workspace shell for $kProductBrandName'),
|
||||
Text(
|
||||
appText(
|
||||
'$kProductBrandName 的工作区外壳',
|
||||
'Workspace shell for $kProductBrandName',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: ValueKey(settings.accountWorkspace),
|
||||
initialValue: settings.accountWorkspace,
|
||||
decoration: const InputDecoration(labelText: 'Workspace Label'),
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('工作区名称', 'Workspace Label'),
|
||||
),
|
||||
onFieldSubmitted: (value) => controller.saveSettings(
|
||||
settings.copyWith(accountWorkspace: value),
|
||||
),
|
||||
@ -103,10 +129,15 @@ class _AccountPageState extends State<AccountPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_tab == 'Sessions')
|
||||
if (_tab == AccountTab.sessions)
|
||||
if (controller.sessions.isEmpty)
|
||||
const SurfaceCard(
|
||||
child: Text('No gateway sessions yet. Connect and start a chat first.'),
|
||||
SurfaceCard(
|
||||
child: Text(
|
||||
appText(
|
||||
'还没有 Gateway 会话。请先连接并开始一次对话。',
|
||||
'No gateway sessions yet. Connect and start a chat first.',
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...controller.sessions.map(
|
||||
@ -121,16 +152,18 @@ class _AccountPageState extends State<AccountPage> {
|
||||
children: [
|
||||
Text(
|
||||
session.label,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${session.surface ?? 'Session'} · ${session.kind ?? 'chat'}',
|
||||
'${session.surface ?? appText('会话', 'Session')} · ${session.kind ?? 'chat'}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(session.model ?? 'gateway'),
|
||||
Text(session.model ?? appText('网关', 'gateway')),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../data/mock_data.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../runtime/runtime_models.dart';
|
||||
import '../../theme/app_palette.dart';
|
||||
@ -25,19 +26,14 @@ class AssistantPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AssistantPageState extends State<AssistantPage> {
|
||||
static const List<String> _modes = ['Craft', 'Ask', 'Plan'];
|
||||
static const Map<String, String> _thinkingModes = {
|
||||
'低': 'low',
|
||||
'中': 'medium',
|
||||
'高': 'high',
|
||||
'超高': 'max',
|
||||
};
|
||||
static const List<String> _modes = ['craft', 'ask', 'plan'];
|
||||
static const List<String> _thinkingModes = ['low', 'medium', 'high', 'max'];
|
||||
|
||||
late final TextEditingController _inputController;
|
||||
late final ScrollController _conversationController;
|
||||
late final FocusNode _composerFocusNode;
|
||||
String _mode = 'Ask';
|
||||
String _thinkingLabel = '高';
|
||||
String _mode = 'ask';
|
||||
String _thinkingLabel = 'high';
|
||||
List<_ComposerAttachment> _attachments = const <_ComposerAttachment>[];
|
||||
String? _lastSubmittedPrompt;
|
||||
String? _lastAutoAgentLabel;
|
||||
@ -177,7 +173,7 @@ class _AssistantPageState extends State<AssistantPage> {
|
||||
items.add(
|
||||
_TimelineItem.message(
|
||||
kind: _TimelineItemKind.user,
|
||||
label: 'You',
|
||||
label: appText('你', 'You'),
|
||||
text: message.text,
|
||||
pending: message.pending,
|
||||
error: message.error,
|
||||
@ -212,21 +208,29 @@ class _AssistantPageState extends State<AssistantPage> {
|
||||
final lastRole = messages.isEmpty ? null : messages.last.role.toLowerCase();
|
||||
if (_lastSubmittedPrompt != null) {
|
||||
final status = hasPendingTask
|
||||
? 'Running'
|
||||
: (lastRole == 'user' ? 'Queued' : 'Completed');
|
||||
? 'running'
|
||||
: (lastRole == 'user' ? 'queued' : 'completed');
|
||||
items.add(
|
||||
_TimelineItem.taskCard(
|
||||
title: _lastSubmittedPrompt!,
|
||||
status: status,
|
||||
summary: switch (status) {
|
||||
'Queued' => 'Submitted to the task queue',
|
||||
'Running' =>
|
||||
'queued' => appText('已提交到任务队列', 'Submitted to the task queue'),
|
||||
'running' => appText(
|
||||
'正在由 ${_lastAutoAgentLabel ?? controller.activeAgentName} 执行',
|
||||
'Executing with ${_lastAutoAgentLabel ?? controller.activeAgentName}',
|
||||
_ => 'Execution finished in this conversation',
|
||||
),
|
||||
_ => appText(
|
||||
'本次会话中的执行已结束',
|
||||
'Execution finished in this conversation',
|
||||
),
|
||||
},
|
||||
detail: _lastSubmittedAttachments.isEmpty
|
||||
? '${controller.currentSessionKey} · ${_lastAutoAgentLabel ?? controller.activeAgentName}'
|
||||
: '${controller.currentSessionKey} · ${_lastSubmittedAttachments.length} attachment(s)',
|
||||
: appText(
|
||||
'${controller.currentSessionKey} · ${_lastSubmittedAttachments.length} 个附件',
|
||||
'${controller.currentSessionKey} · ${_lastSubmittedAttachments.length} attachment(s)',
|
||||
),
|
||||
owner: _lastAutoAgentLabel ?? controller.activeAgentName,
|
||||
sessionKey: controller.currentSessionKey,
|
||||
),
|
||||
@ -294,10 +298,7 @@ class _AssistantPageState extends State<AssistantPage> {
|
||||
_lastSubmittedAttachments = attachmentNames;
|
||||
});
|
||||
|
||||
await controller.sendChatMessage(
|
||||
prompt,
|
||||
thinking: _thinkingModes[_thinkingLabel] ?? 'high',
|
||||
);
|
||||
await controller.sendChatMessage(prompt, thinking: _thinkingLabel);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -380,10 +381,10 @@ class _AssistantPageState extends State<AssistantPage> {
|
||||
'- permission: ${permissionLevel.promptValue}\n\n';
|
||||
|
||||
return switch (mode) {
|
||||
'Craft' =>
|
||||
'craft' =>
|
||||
'$attachmentBlock$executionContext'
|
||||
'Craft a polished result for this request:\n$prompt',
|
||||
'Plan' =>
|
||||
'plan' =>
|
||||
'$attachmentBlock$executionContext'
|
||||
'Create a clear execution plan for this task:\n$prompt',
|
||||
_ => '$attachmentBlock$executionContext$prompt',
|
||||
@ -449,8 +450,14 @@ class _ConversationArea extends StatelessWidget {
|
||||
Text(
|
||||
controller.connection.status ==
|
||||
RuntimeConnectionStatus.connected
|
||||
? 'Describe the task naturally. XWorkmate will route execution.'
|
||||
: 'Connect a gateway to start chatting and running tasks.',
|
||||
? appText(
|
||||
'自然描述任务即可,XWorkmate 会自动路由执行。',
|
||||
'Describe the task naturally. XWorkmate will route execution.',
|
||||
)
|
||||
: appText(
|
||||
'连接 Gateway 后可开始对话和运行任务。',
|
||||
'Connect a gateway to start chatting and running tasks.',
|
||||
),
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
@ -501,10 +508,12 @@ class _ConversationArea extends StatelessWidget {
|
||||
onOpenDetail: () => onOpenDetail(
|
||||
DetailPanelData(
|
||||
title: item.title!,
|
||||
subtitle: 'Tool Call',
|
||||
subtitle: appText('工具调用', 'Tool Call'),
|
||||
icon: Icons.build_circle_outlined,
|
||||
status: StatusInfo(
|
||||
item.pending ? 'Running' : 'Completed',
|
||||
item.pending
|
||||
? appText('运行中', 'Running')
|
||||
: appText('已完成', 'Completed'),
|
||||
item.error
|
||||
? StatusTone.danger
|
||||
: StatusTone.accent,
|
||||
@ -514,7 +523,7 @@ class _ConversationArea extends StatelessWidget {
|
||||
controller.currentSessionKey,
|
||||
controller.activeAgentName,
|
||||
],
|
||||
actions: const ['Copy'],
|
||||
actions: [appText('复制', 'Copy')],
|
||||
sections: const [],
|
||||
),
|
||||
),
|
||||
@ -550,29 +559,35 @@ class _ConversationArea extends StatelessWidget {
|
||||
DetailPanelData _buildTaskDetail(_TimelineItem item) {
|
||||
return DetailPanelData(
|
||||
title: item.title!,
|
||||
subtitle: 'Conversation Task',
|
||||
subtitle: appText('会话任务', 'Conversation Task'),
|
||||
icon: Icons.task_alt_rounded,
|
||||
status: _statusInfoForTask(item.status ?? 'Completed'),
|
||||
status: _statusInfoForTask(item.status ?? 'completed'),
|
||||
description: item.summary ?? '',
|
||||
meta: [
|
||||
item.owner ?? 'Auto route',
|
||||
item.owner ?? appText('自动路由', 'Auto route'),
|
||||
item.sessionKey ?? controller.currentSessionKey,
|
||||
],
|
||||
actions: const ['Continue', 'Open Tasks'],
|
||||
actions: [appText('继续', 'Continue'), appText('打开任务', 'Open Tasks')],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Execution',
|
||||
title: appText('执行', 'Execution'),
|
||||
items: [
|
||||
DetailItem(label: 'Status', value: item.status ?? 'Completed'),
|
||||
DetailItem(
|
||||
label: 'Agent',
|
||||
label: appText('状态', 'Status'),
|
||||
value: _taskStatusLabel(item.status ?? 'completed'),
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('代理', 'Agent'),
|
||||
value: item.owner ?? controller.activeAgentName,
|
||||
),
|
||||
DetailItem(
|
||||
label: 'Session',
|
||||
label: appText('会话', 'Session'),
|
||||
value: item.sessionKey ?? controller.currentSessionKey,
|
||||
),
|
||||
DetailItem(label: 'Detail', value: item.detail ?? 'No detail'),
|
||||
DetailItem(
|
||||
label: appText('详情', 'Detail'),
|
||||
value: item.detail ?? appText('暂无详情', 'No detail'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -632,7 +647,11 @@ class _ComposerBar extends StatelessWidget {
|
||||
permissionLevel == AssistantPermissionLevel.fullAccess
|
||||
? const Color(0xFFFFD5B5)
|
||||
: palette.strokeSoft;
|
||||
final submitLabel = connected ? (mode == 'Ask' ? '提交' : '运行任务') : '连接';
|
||||
final submitLabel = connected
|
||||
? (mode == 'ask'
|
||||
? appText('提交', 'Submit')
|
||||
: appText('运行任务', 'Run Task'))
|
||||
: appText('连接', 'Connect');
|
||||
|
||||
return SurfaceCard(
|
||||
borderRadius: 16,
|
||||
@ -664,8 +683,10 @@ class _ComposerBar extends StatelessWidget {
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
hintText:
|
||||
'Type naturally: run job autopilot, analyze logs, deploy node…',
|
||||
hintText: appText(
|
||||
'直接描述需求:运行任务、分析日志、部署节点……',
|
||||
'Type naturally: run job autopilot, analyze logs, deploy node…',
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => onSend(),
|
||||
),
|
||||
@ -679,7 +700,7 @@ class _ComposerBar extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PopupMenuButton<String>(
|
||||
tooltip: 'Composer actions',
|
||||
tooltip: appText('输入区操作', 'Composer actions'),
|
||||
offset: const Offset(0, -180),
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
@ -687,7 +708,7 @@ class _ComposerBar extends StatelessWidget {
|
||||
onPickAttachments();
|
||||
break;
|
||||
case 'plan':
|
||||
onModeChanged(mode == 'Plan' ? 'Ask' : 'Plan');
|
||||
onModeChanged(mode == 'plan' ? 'ask' : 'plan');
|
||||
break;
|
||||
case 'gateway':
|
||||
onOpenGateway();
|
||||
@ -710,11 +731,15 @@ class _ComposerBar extends StatelessWidget {
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
mode == 'Plan'
|
||||
mode == 'plan'
|
||||
? Icons.task_alt_rounded
|
||||
: Icons.alt_route_rounded,
|
||||
),
|
||||
title: Text(mode == 'Plan' ? '退出计划模式' : '计划模式'),
|
||||
title: Text(
|
||||
mode == 'plan'
|
||||
? appText('退出计划模式', 'Exit plan mode')
|
||||
: appText('计划模式', 'Plan mode'),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
@ -726,7 +751,7 @@ class _ComposerBar extends StatelessWidget {
|
||||
? Icons.lan_rounded
|
||||
: Icons.link_rounded,
|
||||
),
|
||||
title: const Text('连接网关'),
|
||||
title: Text(appText('连接网关', 'Connect gateway')),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
@ -735,7 +760,11 @@ class _ComposerBar extends StatelessWidget {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.hub_rounded),
|
||||
title: Text(
|
||||
autoAgentLabel ?? 'Browser / Coding / Research',
|
||||
autoAgentLabel ??
|
||||
appText(
|
||||
'浏览器 / 编码 / 研究',
|
||||
'Browser / Coding / Research',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -746,7 +775,7 @@ class _ComposerBar extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<AssistantExecutionTarget>(
|
||||
tooltip: 'Execution target',
|
||||
tooltip: appText('执行目标', 'Execution target'),
|
||||
onSelected: (value) {
|
||||
controller.setAssistantExecutionTarget(value);
|
||||
},
|
||||
@ -780,7 +809,7 @@ class _ComposerBar extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<AssistantPermissionLevel>(
|
||||
tooltip: 'Permissions',
|
||||
tooltip: appText('权限', 'Permissions'),
|
||||
onSelected: (value) {
|
||||
controller.setAssistantPermissionLevel(value);
|
||||
},
|
||||
@ -832,35 +861,38 @@ class _ComposerBar extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<String>(
|
||||
tooltip: 'Mode',
|
||||
tooltip: appText('模式', 'Mode'),
|
||||
onSelected: onModeChanged,
|
||||
itemBuilder: (context) => _AssistantPageState._modes
|
||||
.map(
|
||||
(value) => PopupMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
child: Text(_assistantModeLabel(value)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: _ComposerToolbarChip(
|
||||
icon: Icons.tune_rounded,
|
||||
label: mode,
|
||||
label: _assistantModeLabel(mode),
|
||||
showChevron: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
PopupMenuButton<String>(
|
||||
tooltip: 'Reasoning',
|
||||
tooltip: appText('推理强度', 'Reasoning'),
|
||||
onSelected: onThinkingChanged,
|
||||
itemBuilder: (context) => _AssistantPageState
|
||||
._thinkingModes
|
||||
.keys
|
||||
.map(
|
||||
(value) => PopupMenuItem<String>(
|
||||
value: value,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(value)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_assistantThinkingLabel(value),
|
||||
),
|
||||
),
|
||||
if (value == thinkingLabel)
|
||||
const Icon(Icons.check_rounded, size: 18),
|
||||
],
|
||||
@ -870,7 +902,7 @@ class _ComposerBar extends StatelessWidget {
|
||||
.toList(),
|
||||
child: _ComposerToolbarChip(
|
||||
icon: Icons.psychology_alt_outlined,
|
||||
label: thinkingLabel,
|
||||
label: _assistantThinkingLabel(thinkingLabel),
|
||||
showChevron: true,
|
||||
),
|
||||
),
|
||||
@ -896,7 +928,7 @@ class _ComposerBar extends StatelessWidget {
|
||||
children: [
|
||||
Icon(
|
||||
connected
|
||||
? (mode == 'Ask'
|
||||
? (mode == 'ask'
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.play_arrow_rounded)
|
||||
: Icons.link_rounded,
|
||||
@ -1043,7 +1075,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
Text(label, style: theme.textTheme.labelLarge),
|
||||
const SizedBox(height: 6),
|
||||
SelectableText(
|
||||
text.isEmpty ? 'No content yet.' : text,
|
||||
text.isEmpty ? appText('暂无内容。', 'No content yet.') : text,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
height: 1.55,
|
||||
@ -1084,18 +1116,19 @@ class _TaskStatusCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final palette = context.palette;
|
||||
final statusStyle = _pillStyleForStatus(context, status);
|
||||
final icon = switch (status) {
|
||||
'Queued' => Icons.schedule_send_rounded,
|
||||
'Running' => Icons.play_circle_outline_rounded,
|
||||
'Failed' => Icons.error_outline_rounded,
|
||||
final normalizedStatus = _normalizedTaskStatus(status);
|
||||
final statusStyle = _pillStyleForStatus(context, normalizedStatus);
|
||||
final icon = switch (normalizedStatus) {
|
||||
'queued' => Icons.schedule_send_rounded,
|
||||
'running' => Icons.play_circle_outline_rounded,
|
||||
'failed' => Icons.error_outline_rounded,
|
||||
_ => Icons.task_alt_rounded,
|
||||
};
|
||||
final hint = switch (status) {
|
||||
'Queued' => 'Waiting in queue',
|
||||
'Running' => 'Working now',
|
||||
'Failed' => 'Needs attention',
|
||||
_ => 'Continue in session',
|
||||
final hint = switch (normalizedStatus) {
|
||||
'queued' => appText('排队等待执行', 'Waiting in queue'),
|
||||
'running' => appText('正在执行中', 'Working now'),
|
||||
'failed' => appText('需要处理', 'Needs attention'),
|
||||
_ => appText('可继续在当前会话处理', 'Continue in session'),
|
||||
};
|
||||
|
||||
return Align(
|
||||
@ -1150,7 +1183,7 @@ class _TaskStatusCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
_StatusPill(
|
||||
label: status,
|
||||
label: _taskStatusLabel(status),
|
||||
backgroundColor: statusStyle.backgroundColor,
|
||||
textColor: statusStyle.foregroundColor,
|
||||
),
|
||||
@ -1201,12 +1234,14 @@ class _TaskStatusCard extends StatelessWidget {
|
||||
size: 16,
|
||||
),
|
||||
label: Text(
|
||||
isCurrentSession ? 'Continue' : 'Open Session',
|
||||
isCurrentSession
|
||||
? appText('继续', 'Continue')
|
||||
: appText('打开会话', 'Open Session'),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onOpenTasks,
|
||||
child: const Text('Open Tasks'),
|
||||
child: Text(appText('打开任务', 'Open Tasks')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -1246,11 +1281,11 @@ class _ToolCallTileState extends State<_ToolCallTile> {
|
||||
final theme = Theme.of(context);
|
||||
final palette = context.palette;
|
||||
final statusLabel = widget.pending
|
||||
? 'Running'
|
||||
: (widget.error ? 'Error' : 'Completed');
|
||||
? 'running'
|
||||
: (widget.error ? 'error' : 'completed');
|
||||
final statusStyle = _pillStyleForStatus(context, statusLabel);
|
||||
final collapsedSummary = widget.summary.trim().isEmpty
|
||||
? 'Tool call in progress.'
|
||||
? appText('工具调用进行中。', 'Tool call in progress.')
|
||||
: widget.summary.trim().replaceAll('\n', ' ');
|
||||
|
||||
return Align(
|
||||
@ -1307,7 +1342,7 @@ class _ToolCallTileState extends State<_ToolCallTile> {
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
_StatusPill(
|
||||
label: statusLabel,
|
||||
label: _toolCallStatusLabel(statusLabel),
|
||||
backgroundColor: statusStyle.backgroundColor,
|
||||
textColor: statusStyle.foregroundColor,
|
||||
),
|
||||
@ -1337,14 +1372,17 @@ class _ToolCallTileState extends State<_ToolCallTile> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.summary.trim().isEmpty
|
||||
? 'Tool call in progress.'
|
||||
? appText(
|
||||
'工具调用进行中。',
|
||||
'Tool call in progress.',
|
||||
)
|
||||
: widget.summary.trim(),
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
TextButton(
|
||||
onPressed: widget.onOpenDetail,
|
||||
child: const Text('Open detail'),
|
||||
child: Text(appText('打开详情', 'Open detail')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -1416,7 +1454,7 @@ class _ConnectionChip extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'${connection.status.label} · ${connection.remoteAddress ?? 'No target'}',
|
||||
'${connection.status.label} · ${connection.remoteAddress ?? appText('未连接目标', 'No target')}',
|
||||
style: theme.textTheme.labelLarge,
|
||||
),
|
||||
);
|
||||
@ -1525,16 +1563,17 @@ class _PillStyle {
|
||||
|
||||
_PillStyle _pillStyleForStatus(BuildContext context, String label) {
|
||||
final theme = Theme.of(context);
|
||||
return switch (label) {
|
||||
'Running' => _PillStyle(
|
||||
final normalized = _normalizedTaskStatus(label);
|
||||
return switch (normalized) {
|
||||
'running' => _PillStyle(
|
||||
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.10),
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
),
|
||||
'Queued' => _PillStyle(
|
||||
'queued' => _PillStyle(
|
||||
backgroundColor: theme.colorScheme.secondary.withValues(alpha: 0.10),
|
||||
foregroundColor: theme.colorScheme.secondary,
|
||||
),
|
||||
'Failed' || 'Error' => _PillStyle(
|
||||
'failed' || 'error' => _PillStyle(
|
||||
backgroundColor: theme.colorScheme.error.withValues(alpha: 0.10),
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
),
|
||||
@ -1546,10 +1585,46 @@ _PillStyle _pillStyleForStatus(BuildContext context, String label) {
|
||||
}
|
||||
|
||||
StatusInfo _statusInfoForTask(String status) => switch (status) {
|
||||
'Running' => const StatusInfo('Running', StatusTone.accent),
|
||||
'Failed' => const StatusInfo('Failed', StatusTone.danger),
|
||||
'Queued' => const StatusInfo('Queued', StatusTone.neutral),
|
||||
_ => const StatusInfo('Completed', StatusTone.success),
|
||||
'running' ||
|
||||
'Running' => StatusInfo(appText('运行中', 'Running'), StatusTone.accent),
|
||||
'failed' ||
|
||||
'Failed' => StatusInfo(appText('失败', 'Failed'), StatusTone.danger),
|
||||
'queued' ||
|
||||
'Queued' => StatusInfo(appText('排队中', 'Queued'), StatusTone.neutral),
|
||||
_ => StatusInfo(appText('已完成', 'Completed'), StatusTone.success),
|
||||
};
|
||||
|
||||
String _normalizedTaskStatus(String status) {
|
||||
final value = status.trim().toLowerCase();
|
||||
return switch (value) {
|
||||
'running' => 'running',
|
||||
'queued' => 'queued',
|
||||
'failed' => 'failed',
|
||||
'error' => 'error',
|
||||
_ => 'completed',
|
||||
};
|
||||
}
|
||||
|
||||
String _taskStatusLabel(String status) => _statusInfoForTask(status).label;
|
||||
|
||||
String _toolCallStatusLabel(String status) =>
|
||||
switch (_normalizedTaskStatus(status)) {
|
||||
'running' => appText('运行中', 'Running'),
|
||||
'failed' || 'error' => appText('错误', 'Error'),
|
||||
_ => appText('已完成', 'Completed'),
|
||||
};
|
||||
|
||||
String _assistantModeLabel(String mode) => switch (mode) {
|
||||
'craft' => appText('创作', 'Craft'),
|
||||
'plan' => appText('计划', 'Plan'),
|
||||
_ => appText('问答', 'Ask'),
|
||||
};
|
||||
|
||||
String _assistantThinkingLabel(String level) => switch (level) {
|
||||
'low' => appText('低', 'Low'),
|
||||
'medium' => appText('中', 'Medium'),
|
||||
'max' => appText('超高', 'Max'),
|
||||
_ => appText('高', 'High'),
|
||||
};
|
||||
|
||||
class _ComposerAttachment {
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../data/mock_data.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../runtime/runtime_controllers.dart';
|
||||
import '../../runtime/runtime_models.dart';
|
||||
@ -28,27 +29,30 @@ class ModulesPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ModulesPageState extends State<ModulesPage> {
|
||||
String _tab = 'Gateway';
|
||||
ModulesTab _tab = ModulesTab.gateway;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = widget.controller;
|
||||
final metrics = [
|
||||
MetricSummary(
|
||||
label: 'Gateway',
|
||||
label: appText('网关', 'Gateway'),
|
||||
value: controller.connection.status.label,
|
||||
caption: controller.connection.remoteAddress ?? kAppVersionLabel,
|
||||
icon: Icons.wifi_tethering_rounded,
|
||||
status: _connectionStatus(controller.connection.status),
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Nodes',
|
||||
label: appText('节点', 'Nodes'),
|
||||
value: '${controller.instances.length}',
|
||||
caption: '${controller.instances.where((item) => item.mode == 'active').length} active',
|
||||
caption: appText(
|
||||
'${controller.instances.where((item) => item.mode == 'active').length} 个活跃实例',
|
||||
'${controller.instances.where((item) => item.mode == 'active').length} active',
|
||||
),
|
||||
icon: Icons.developer_board_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Agents',
|
||||
label: appText('代理', 'Agents'),
|
||||
value: '${controller.agents.length}',
|
||||
caption: controller.activeAgentName,
|
||||
icon: Icons.hub_rounded,
|
||||
@ -64,9 +68,11 @@ class _ModulesPageState extends State<ModulesPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
title: 'Modules',
|
||||
subtitle:
|
||||
'Manage gateway, agents, nodes, skills, and platform services.',
|
||||
title: appText('模块', 'Modules'),
|
||||
subtitle: appText(
|
||||
'管理 Gateway、代理、节点、技能和平台服务。',
|
||||
'Manage gateway, agents, nodes, skills, and platform services.',
|
||||
),
|
||||
trailing: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
@ -74,8 +80,8 @@ class _ModulesPageState extends State<ModulesPage> {
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '搜索',
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索模块', 'Search modules'),
|
||||
prefixIcon: Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
@ -95,27 +101,23 @@ class _ModulesPageState extends State<ModulesPage> {
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => controller.navigateTo(
|
||||
WorkspaceDestination.settings,
|
||||
),
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.settings),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('接入模块'),
|
||||
label: Text(appText('接入模块', 'Add Module')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SectionTabs(
|
||||
items: const [
|
||||
'Gateway',
|
||||
'Nodes',
|
||||
'Agents',
|
||||
'Skills',
|
||||
'ClawHub',
|
||||
'Connectors',
|
||||
],
|
||||
value: _tab,
|
||||
onChanged: (value) => setState(() => _tab = value),
|
||||
items: ModulesTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (value) => setState(
|
||||
() => _tab = ModulesTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
LayoutBuilder(
|
||||
@ -141,27 +143,28 @@ class _ModulesPageState extends State<ModulesPage> {
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
switch (_tab) {
|
||||
'Gateway' => _GatewayPanel(
|
||||
ModulesTab.gateway => _GatewayPanel(
|
||||
controller: controller,
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
'Nodes' => _NodesPanel(
|
||||
ModulesTab.nodes => _NodesPanel(
|
||||
controller: controller,
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
'Agents' => _AgentsPanel(
|
||||
ModulesTab.agents => _AgentsPanel(
|
||||
controller: controller,
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
'Skills' => _SkillsPanel(
|
||||
ModulesTab.skills => _SkillsPanel(
|
||||
controller: controller,
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
'ClawHub' => _FallbackHubPanel(onOpenDetail: widget.onOpenDetail),
|
||||
'Connectors' => _FallbackConnectorsPanel(
|
||||
ModulesTab.clawHub => _FallbackHubPanel(
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
ModulesTab.connectors => _FallbackConnectorsPanel(
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
},
|
||||
],
|
||||
),
|
||||
@ -172,10 +175,7 @@ class _ModulesPageState extends State<ModulesPage> {
|
||||
}
|
||||
|
||||
class _GatewayPanel extends StatelessWidget {
|
||||
const _GatewayPanel({
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
const _GatewayPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
@ -185,29 +185,33 @@ class _GatewayPanel extends StatelessWidget {
|
||||
final connection = controller.connection;
|
||||
final metrics = [
|
||||
MetricSummary(
|
||||
label: 'Mode',
|
||||
label: appText('模式', 'Mode'),
|
||||
value: controller.settings.gateway.mode.label,
|
||||
caption: controller.settings.gateway.useSetupCode
|
||||
? 'Setup code'
|
||||
: 'Manual profile',
|
||||
? appText('配置码', 'Setup code')
|
||||
: appText('手动配置', 'Manual profile'),
|
||||
icon: Icons.link_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Active Sessions',
|
||||
label: appText('活跃会话', 'Active Sessions'),
|
||||
value: '${controller.sessions.length}',
|
||||
caption: 'Current key ${controller.currentSessionKey}',
|
||||
caption: appText(
|
||||
'当前 Key ${controller.currentSessionKey}',
|
||||
'Current key ${controller.currentSessionKey}',
|
||||
),
|
||||
icon: Icons.chat_bubble_outline_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Today Runs',
|
||||
value: '${controller.tasksController.running.length + controller.tasksController.history.length}',
|
||||
caption: 'Derived from live session activity',
|
||||
label: appText('今日运行', 'Today Runs'),
|
||||
value:
|
||||
'${controller.tasksController.running.length + controller.tasksController.history.length}',
|
||||
caption: appText('根据实时会话活动计算', 'Derived from live session activity'),
|
||||
icon: Icons.bolt_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Skills',
|
||||
label: appText('技能', 'Skills'),
|
||||
value: '${controller.skills.length}',
|
||||
caption: 'Loaded from gateway',
|
||||
caption: appText('来自网关加载', 'Loaded from gateway'),
|
||||
icon: Icons.extension_rounded,
|
||||
),
|
||||
];
|
||||
@ -243,28 +247,43 @@ class _GatewayPanel extends StatelessWidget {
|
||||
SurfaceCard(
|
||||
onTap: () => onOpenDetail(
|
||||
DetailPanelData(
|
||||
title: 'Gateway Overview',
|
||||
subtitle: 'Runtime',
|
||||
title: appText('网关概览', 'Gateway Overview'),
|
||||
subtitle: appText('运行时', 'Runtime'),
|
||||
icon: Icons.wifi_tethering_rounded,
|
||||
status: _connectionStatus(connection.status),
|
||||
description:
|
||||
'Live gateway control plane summary aligned with the macOS workspace shell.',
|
||||
description: appText(
|
||||
'与 macOS 工作台保持一致的实时 Gateway 控制面摘要。',
|
||||
'Live gateway control plane summary aligned with the macOS workspace shell.',
|
||||
),
|
||||
meta: [
|
||||
connection.remoteAddress ?? 'No target',
|
||||
connection.remoteAddress ?? appText('未连接目标', 'No target'),
|
||||
controller.activeAgentName,
|
||||
],
|
||||
actions: const ['Refresh', 'Open Settings'],
|
||||
actions: [
|
||||
appText('刷新', 'Refresh'),
|
||||
appText('打开设置', 'Open Settings'),
|
||||
],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Connection',
|
||||
title: appText('连接', 'Connection'),
|
||||
items: [
|
||||
DetailItem(label: 'Status', value: connection.status.label),
|
||||
DetailItem(
|
||||
label: 'Address',
|
||||
value: connection.remoteAddress ?? 'Offline',
|
||||
label: appText('状态', 'Status'),
|
||||
value: connection.status.label,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('地址', 'Address'),
|
||||
value:
|
||||
connection.remoteAddress ?? appText('离线', 'Offline'),
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('模式', 'Mode'),
|
||||
value: controller.settings.gateway.mode.label,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('代理', 'Agent'),
|
||||
value: controller.activeAgentName,
|
||||
),
|
||||
DetailItem(label: 'Mode', value: controller.settings.gateway.mode.label),
|
||||
DetailItem(label: 'Agent', value: controller.activeAgentName),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -273,10 +292,13 @@ class _GatewayPanel extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Gateway', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('网关', 'Gateway'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'${connection.status.label} · ${connection.remoteAddress ?? 'No target'} · ${controller.activeAgentName}',
|
||||
'${connection.status.label} · ${connection.remoteAddress ?? appText('未连接目标', 'No target')} · ${controller.activeAgentName}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
@ -286,17 +308,16 @@ class _GatewayPanel extends StatelessWidget {
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: controller.refreshGatewayHealth,
|
||||
child: const Text('刷新状态'),
|
||||
child: Text(appText('刷新状态', 'Refresh status')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: controller.refreshSessions,
|
||||
child: const Text('刷新会话'),
|
||||
child: Text(appText('刷新会话', 'Refresh sessions')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => controller.navigateTo(
|
||||
WorkspaceDestination.settings,
|
||||
),
|
||||
child: const Text('配置'),
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.settings),
|
||||
child: Text(appText('配置', 'Configure')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -308,16 +329,23 @@ class _GatewayPanel extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('状态摘要', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('状态摘要', 'Status Summary'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_KeyValueLine(
|
||||
label: 'Health',
|
||||
value: healthPayload.isEmpty ? 'Unavailable' : encodePrettyJson(healthPayload),
|
||||
value: healthPayload.isEmpty
|
||||
? appText('不可用', 'Unavailable')
|
||||
: encodePrettyJson(healthPayload),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_KeyValueLine(
|
||||
label: 'Status',
|
||||
value: statusPayload.isEmpty ? 'Unavailable' : encodePrettyJson(statusPayload),
|
||||
value: statusPayload.isEmpty
|
||||
? appText('不可用', 'Unavailable')
|
||||
: encodePrettyJson(statusPayload),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -328,10 +356,7 @@ class _GatewayPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _NodesPanel extends StatelessWidget {
|
||||
const _NodesPanel({
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
const _NodesPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
@ -343,16 +368,22 @@ class _NodesPanel extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SectionHeader(
|
||||
title: 'Nodes',
|
||||
subtitle: 'Live system-presence data from the gateway runtime.',
|
||||
title: appText('节点', 'Nodes'),
|
||||
subtitle: appText(
|
||||
'来自 Gateway 运行时的在线实例与存在性数据。',
|
||||
'Live system-presence data from the gateway runtime.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (items.isEmpty)
|
||||
SurfaceCard(
|
||||
child: Text(
|
||||
controller.connection.status == RuntimeConnectionStatus.connected
|
||||
? 'No live instances reported yet.'
|
||||
: 'Connect a gateway to load instances / presence.',
|
||||
? appText('暂时还没有上报在线实例。', 'No live instances reported yet.')
|
||||
: appText(
|
||||
'连接 Gateway 后可加载实例与在线状态。',
|
||||
'Connect a gateway to load instances / presence.',
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
@ -363,24 +394,30 @@ class _NodesPanel extends StatelessWidget {
|
||||
onTap: () => onOpenDetail(
|
||||
DetailPanelData(
|
||||
title: node.host ?? node.id,
|
||||
subtitle: 'Instance',
|
||||
subtitle: appText('实例', 'Instance'),
|
||||
icon: Icons.developer_board_rounded,
|
||||
status: _instanceStatus(node),
|
||||
description: node.text,
|
||||
meta: [
|
||||
node.platform ?? 'unknown',
|
||||
node.deviceFamily ?? 'unknown',
|
||||
node.platform ?? appText('未知', 'unknown'),
|
||||
node.deviceFamily ?? appText('未知', 'unknown'),
|
||||
],
|
||||
actions: const ['Refresh'],
|
||||
actions: [appText('刷新', 'Refresh')],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Runtime',
|
||||
title: appText('运行时', 'Runtime'),
|
||||
items: [
|
||||
DetailItem(label: 'IP', value: node.ip ?? 'n/a'),
|
||||
DetailItem(label: 'Version', value: node.version ?? 'n/a'),
|
||||
DetailItem(label: 'Mode', value: node.mode ?? 'n/a'),
|
||||
DetailItem(
|
||||
label: 'Last Input',
|
||||
label: 'Version',
|
||||
value: node.version ?? 'n/a',
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('模式', 'Mode'),
|
||||
value: node.mode ?? 'n/a',
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('最近输入', 'Last Input'),
|
||||
value: node.lastInputSeconds == null
|
||||
? 'n/a'
|
||||
: '${node.lastInputSeconds}s',
|
||||
@ -403,7 +440,7 @@ class _NodesPanel extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${node.platform ?? 'unknown'} · ${node.deviceFamily ?? 'unknown'}',
|
||||
'${node.platform ?? appText('未知', 'unknown')} · ${node.deviceFamily ?? appText('未知', 'unknown')}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
@ -427,10 +464,7 @@ class _NodesPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _AgentsPanel extends StatelessWidget {
|
||||
const _AgentsPanel({
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
const _AgentsPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
@ -449,8 +483,14 @@ class _AgentsPanel extends StatelessWidget {
|
||||
return SurfaceCard(
|
||||
child: Text(
|
||||
controller.connection.status == RuntimeConnectionStatus.connected
|
||||
? 'No agents reported by the gateway.'
|
||||
: 'Connect a gateway to load agents.',
|
||||
? appText(
|
||||
'网关当前没有返回代理列表。',
|
||||
'No agents reported by the gateway.',
|
||||
)
|
||||
: appText(
|
||||
'连接 Gateway 后可加载代理。',
|
||||
'Connect a gateway to load agents.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -465,21 +505,39 @@ class _AgentsPanel extends StatelessWidget {
|
||||
onTap: () => onOpenDetail(
|
||||
DetailPanelData(
|
||||
title: agent.name,
|
||||
subtitle: 'Agent',
|
||||
subtitle: appText('代理', 'Agent'),
|
||||
icon: Icons.hub_rounded,
|
||||
status: controller.selectedAgentId == agent.id
|
||||
? const StatusInfo('Selected', StatusTone.accent)
|
||||
: const StatusInfo('Available', StatusTone.success),
|
||||
description: 'Gateway operator agent available for session routing.',
|
||||
? StatusInfo(
|
||||
appText('已选中', 'Selected'),
|
||||
StatusTone.accent,
|
||||
)
|
||||
: StatusInfo(
|
||||
appText('可用', 'Available'),
|
||||
StatusTone.success,
|
||||
),
|
||||
description: appText(
|
||||
'可用于会话路由的 Gateway 执行代理。',
|
||||
'Gateway operator agent available for session routing.',
|
||||
),
|
||||
meta: [agent.id, agent.theme],
|
||||
actions: const ['Select', 'Open Session'],
|
||||
actions: [
|
||||
appText('选择', 'Select'),
|
||||
appText('打开会话', 'Open Session'),
|
||||
],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Identity',
|
||||
title: appText('身份信息', 'Identity'),
|
||||
items: [
|
||||
DetailItem(label: 'Name', value: agent.name),
|
||||
DetailItem(
|
||||
label: appText('名称', 'Name'),
|
||||
value: agent.name,
|
||||
),
|
||||
DetailItem(label: 'ID', value: agent.id),
|
||||
DetailItem(label: 'Theme', value: agent.theme),
|
||||
DetailItem(
|
||||
label: appText('主题', 'Theme'),
|
||||
value: agent.theme,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -498,14 +556,23 @@ class _AgentsPanel extends StatelessWidget {
|
||||
),
|
||||
StatusBadge(
|
||||
status: controller.selectedAgentId == agent.id
|
||||
? const StatusInfo('Selected', StatusTone.accent)
|
||||
: const StatusInfo('Ready', StatusTone.success),
|
||||
? StatusInfo(
|
||||
appText('已选中', 'Selected'),
|
||||
StatusTone.accent,
|
||||
)
|
||||
: StatusInfo(
|
||||
appText('就绪', 'Ready'),
|
||||
StatusTone.success,
|
||||
),
|
||||
compact: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text('ID: ${agent.id}', style: Theme.of(context).textTheme.bodyMedium),
|
||||
Text(
|
||||
'ID: ${agent.id}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@ -513,11 +580,11 @@ class _AgentsPanel extends StatelessWidget {
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () => controller.selectAgent(agent.id),
|
||||
child: const Text('选择'),
|
||||
child: Text(appText('选择', 'Select')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => controller.refreshSessions(),
|
||||
child: const Text('打开'),
|
||||
child: Text(appText('打开', 'Open')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -534,10 +601,7 @@ class _AgentsPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _SkillsPanel extends StatelessWidget {
|
||||
const _SkillsPanel({
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
const _SkillsPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
@ -549,8 +613,14 @@ class _SkillsPanel extends StatelessWidget {
|
||||
return SurfaceCard(
|
||||
child: Text(
|
||||
controller.connection.status == RuntimeConnectionStatus.connected
|
||||
? 'No skills loaded for the active gateway / agent.'
|
||||
: 'Connect a gateway to load skills.',
|
||||
? appText(
|
||||
'当前网关或代理没有加载技能。',
|
||||
'No skills loaded for the active gateway / agent.',
|
||||
)
|
||||
: appText(
|
||||
'连接 Gateway 后可加载技能。',
|
||||
'Connect a gateway to load skills.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -564,34 +634,40 @@ class _SkillsPanel extends StatelessWidget {
|
||||
onTap: () => onOpenDetail(
|
||||
DetailPanelData(
|
||||
title: skill.name,
|
||||
subtitle: 'Skill',
|
||||
subtitle: appText('技能', 'Skill'),
|
||||
icon: Icons.extension_rounded,
|
||||
status: skill.disabled
|
||||
? const StatusInfo('Disabled', StatusTone.warning)
|
||||
: const StatusInfo('Enabled', StatusTone.success),
|
||||
? StatusInfo(
|
||||
appText('已禁用', 'Disabled'),
|
||||
StatusTone.warning,
|
||||
)
|
||||
: StatusInfo(
|
||||
appText('已启用', 'Enabled'),
|
||||
StatusTone.success,
|
||||
),
|
||||
description: skill.description,
|
||||
meta: [skill.source, skill.skillKey],
|
||||
actions: const ['Refresh'],
|
||||
actions: [appText('刷新', 'Refresh')],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Requirements',
|
||||
title: appText('依赖要求', 'Requirements'),
|
||||
items: [
|
||||
DetailItem(
|
||||
label: 'Missing bins',
|
||||
label: appText('缺失二进制', 'Missing bins'),
|
||||
value: skill.missingBins.isEmpty
|
||||
? 'None'
|
||||
? appText('无', 'None')
|
||||
: skill.missingBins.join(', '),
|
||||
),
|
||||
DetailItem(
|
||||
label: 'Missing env',
|
||||
label: appText('缺失环境变量', 'Missing env'),
|
||||
value: skill.missingEnv.isEmpty
|
||||
? 'None'
|
||||
? appText('无', 'None')
|
||||
: skill.missingEnv.join(', '),
|
||||
),
|
||||
DetailItem(
|
||||
label: 'Missing config',
|
||||
label: appText('缺失配置', 'Missing config'),
|
||||
value: skill.missingConfig.isEmpty
|
||||
? 'None'
|
||||
? appText('无', 'None')
|
||||
: skill.missingConfig.join(', '),
|
||||
),
|
||||
],
|
||||
@ -622,8 +698,14 @@ class _SkillsPanel extends StatelessWidget {
|
||||
flex: 2,
|
||||
child: StatusBadge(
|
||||
status: skill.disabled
|
||||
? const StatusInfo('Disabled', StatusTone.warning)
|
||||
: const StatusInfo('Enabled', StatusTone.success),
|
||||
? StatusInfo(
|
||||
appText('已禁用', 'Disabled'),
|
||||
StatusTone.warning,
|
||||
)
|
||||
: StatusInfo(
|
||||
appText('已启用', 'Enabled'),
|
||||
StatusTone.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(flex: 2, child: Text(skill.source)),
|
||||
@ -661,7 +743,10 @@ class _FallbackHubPanel extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.name, style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
item.name,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(item.description),
|
||||
],
|
||||
@ -733,7 +818,10 @@ class _FallbackConnectorsPanel extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
StatusBadge(status: connector.status, compact: true),
|
||||
StatusBadge(
|
||||
status: connector.status,
|
||||
compact: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
@ -751,10 +839,7 @@ class _FallbackConnectorsPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _KeyValueLine extends StatelessWidget {
|
||||
const _KeyValueLine({
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
const _KeyValueLine({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
@ -780,20 +865,33 @@ class _KeyValueLine extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
StatusInfo _connectionStatus(RuntimeConnectionStatus status) => switch (status) {
|
||||
RuntimeConnectionStatus.connected => const StatusInfo('Healthy', StatusTone.success),
|
||||
RuntimeConnectionStatus.connecting => const StatusInfo('Connecting', StatusTone.accent),
|
||||
RuntimeConnectionStatus.error => const StatusInfo('Error', StatusTone.danger),
|
||||
RuntimeConnectionStatus.offline => const StatusInfo('Offline', StatusTone.neutral),
|
||||
};
|
||||
StatusInfo _connectionStatus(RuntimeConnectionStatus status) =>
|
||||
switch (status) {
|
||||
RuntimeConnectionStatus.connected => StatusInfo(
|
||||
appText('健康', 'Healthy'),
|
||||
StatusTone.success,
|
||||
),
|
||||
RuntimeConnectionStatus.connecting => StatusInfo(
|
||||
appText('连接中', 'Connecting'),
|
||||
StatusTone.accent,
|
||||
),
|
||||
RuntimeConnectionStatus.error => StatusInfo(
|
||||
appText('错误', 'Error'),
|
||||
StatusTone.danger,
|
||||
),
|
||||
RuntimeConnectionStatus.offline => StatusInfo(
|
||||
appText('离线', 'Offline'),
|
||||
StatusTone.neutral,
|
||||
),
|
||||
};
|
||||
|
||||
StatusInfo _instanceStatus(GatewayInstanceSummary item) {
|
||||
final mode = (item.mode ?? '').toLowerCase();
|
||||
if (mode.contains('error') || mode.contains('warn')) {
|
||||
return const StatusInfo('Warning', StatusTone.warning);
|
||||
return StatusInfo(appText('告警', 'Warning'), StatusTone.warning);
|
||||
}
|
||||
if (mode.contains('active') || mode.contains('online')) {
|
||||
return const StatusInfo('Online', StatusTone.success);
|
||||
return StatusInfo(appText('在线', 'Online'), StatusTone.success);
|
||||
}
|
||||
return const StatusInfo('Seen', StatusTone.neutral);
|
||||
return StatusInfo(appText('已发现', 'Seen'), StatusTone.neutral);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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 '../../widgets/metric_card.dart';
|
||||
@ -25,7 +26,7 @@ class SecretsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SecretsPageState extends State<SecretsPage> {
|
||||
String _tab = 'Vault';
|
||||
SecretsTab _tab = SecretsTab.vault;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -39,9 +40,11 @@ class _SecretsPageState extends State<SecretsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
title: 'Secrets',
|
||||
subtitle:
|
||||
'Manage secret providers, credentials, and secure references across modules.',
|
||||
title: appText('密钥', 'Secrets'),
|
||||
subtitle: appText(
|
||||
'管理密钥提供方、凭证和模块间的安全引用。',
|
||||
'Manage secret providers, credentials, and secure references across modules.',
|
||||
),
|
||||
trailing: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
@ -49,8 +52,8 @@ class _SecretsPageState extends State<SecretsPage> {
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '搜索',
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索密钥', 'Search secrets'),
|
||||
prefixIcon: Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
@ -63,40 +66,42 @@ class _SecretsPageState extends State<SecretsPage> {
|
||||
icon: const Icon(Icons.sync_rounded),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => controller.navigateTo(
|
||||
WorkspaceDestination.settings,
|
||||
),
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.settings),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('Add Secret'),
|
||||
label: Text(appText('新增密钥', 'Add Secret')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SectionTabs(
|
||||
items: const ['Vault', 'Local Store', 'Providers', 'Audit'],
|
||||
value: _tab,
|
||||
onChanged: (value) => setState(() => _tab = value),
|
||||
items: SecretsTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (value) => setState(
|
||||
() => _tab = SecretsTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
switch (_tab) {
|
||||
'Vault' => _VaultPanel(
|
||||
SecretsTab.vault => _VaultPanel(
|
||||
controller: controller,
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
'Local Store' => _LocalStorePanel(
|
||||
SecretsTab.localStore => _LocalStorePanel(
|
||||
controller: controller,
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
'Providers' => _ProvidersPanel(
|
||||
SecretsTab.providers => _ProvidersPanel(
|
||||
controller: controller,
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
'Audit' => _AuditPanel(
|
||||
SecretsTab.audit => _AuditPanel(
|
||||
controller: controller,
|
||||
onOpenDetail: widget.onOpenDetail,
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
},
|
||||
],
|
||||
),
|
||||
@ -107,10 +112,7 @@ class _SecretsPageState extends State<SecretsPage> {
|
||||
}
|
||||
|
||||
class _VaultPanel extends StatelessWidget {
|
||||
const _VaultPanel({
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
const _VaultPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
@ -120,22 +122,23 @@ class _VaultPanel extends StatelessWidget {
|
||||
final vault = controller.settings.vault;
|
||||
final metrics = [
|
||||
MetricSummary(
|
||||
label: 'Provider',
|
||||
label: appText('提供方', 'Provider'),
|
||||
value: 'Vault',
|
||||
caption: controller.settingsController.vaultStatus,
|
||||
icon: Icons.key_rounded,
|
||||
status: _statusForString(controller.settingsController.vaultStatus),
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Token Ref',
|
||||
label: appText('Token 引用', 'Token Ref'),
|
||||
value: vault.tokenRef,
|
||||
caption: 'Stored via secure refs',
|
||||
caption: appText('通过安全引用保存', 'Stored via secure refs'),
|
||||
icon: Icons.lock_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Secret Refs',
|
||||
value: '${controller.secretReferences.where((item) => item.provider == 'Vault').length}',
|
||||
caption: 'Referenced by modules',
|
||||
label: appText('密钥引用', 'Secret Refs'),
|
||||
value:
|
||||
'${controller.secretReferences.where((item) => item.provider == 'Vault').length}',
|
||||
caption: appText('被模块引用', 'Referenced by modules'),
|
||||
icon: Icons.link_rounded,
|
||||
),
|
||||
];
|
||||
@ -169,10 +172,16 @@ class _VaultPanel extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Vault Server', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('Vault 服务', 'Vault Server'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Address: ${vault.address}\nNamespace: ${vault.namespace}\nAuth mode: ${vault.authMode}\nToken ref: ${vault.tokenRef}',
|
||||
'${appText('地址', 'Address')}: ${vault.address}\n'
|
||||
'${appText('命名空间', 'Namespace')}: ${vault.namespace}\n'
|
||||
'${appText('认证模式', 'Auth mode')}: ${vault.authMode}\n'
|
||||
'${appText('Token 引用', 'Token ref')}: ${vault.tokenRef}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@ -182,13 +191,12 @@ class _VaultPanel extends StatelessWidget {
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: controller.testVaultConnection,
|
||||
child: const Text('连接测试'),
|
||||
child: Text(appText('连接测试', 'Test Connection')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => controller.navigateTo(
|
||||
WorkspaceDestination.settings,
|
||||
),
|
||||
child: const Text('配置'),
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.settings),
|
||||
child: Text(appText('配置', 'Configure')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -197,8 +205,11 @@ class _VaultPanel extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SectionHeader(
|
||||
title: '引用列表',
|
||||
subtitle: '只展示 masked reference,不暴露真实 secret value。',
|
||||
title: appText('引用列表', 'Reference List'),
|
||||
subtitle: appText(
|
||||
'仅展示脱敏引用,不暴露真实密钥值。',
|
||||
'Only masked references are shown, never raw secret values.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_SecretRefsTable(
|
||||
@ -226,23 +237,23 @@ class _LocalStorePanel extends StatelessWidget {
|
||||
final refs = controller.secretReferences;
|
||||
final metrics = [
|
||||
MetricSummary(
|
||||
label: 'Local Store',
|
||||
value: 'Enabled',
|
||||
label: appText('本地存储', 'Local Store'),
|
||||
value: appText('已启用', 'Enabled'),
|
||||
caption: 'flutter_secure_storage + shared prefs',
|
||||
icon: Icons.lock_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Entries',
|
||||
label: appText('条目数', 'Entries'),
|
||||
value: '${refs.length}',
|
||||
caption: 'masked secret references',
|
||||
caption: appText('脱敏密钥引用', 'Masked secret references'),
|
||||
icon: Icons.key_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Last Audit',
|
||||
label: appText('最近审计', 'Last Audit'),
|
||||
value: controller.secretAuditTrail.isEmpty
|
||||
? 'None'
|
||||
? appText('无', 'None')
|
||||
: controller.secretAuditTrail.first.timeLabel,
|
||||
caption: '最近一次安全操作',
|
||||
caption: appText('最近一次安全操作', 'Most recent security action'),
|
||||
icon: Icons.schedule_rounded,
|
||||
),
|
||||
];
|
||||
@ -279,10 +290,7 @@ class _LocalStorePanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _ProvidersPanel extends StatelessWidget {
|
||||
const _ProvidersPanel({
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
const _ProvidersPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
@ -292,26 +300,38 @@ class _ProvidersPanel extends StatelessWidget {
|
||||
final providers = [
|
||||
_ProviderCardData(
|
||||
name: 'HashiCorp Vault',
|
||||
description: 'Namespace-aware Vault integration with token refs.',
|
||||
description: appText(
|
||||
'支持命名空间和 token 引用的 Vault 集成。',
|
||||
'Namespace-aware Vault integration with token refs.',
|
||||
),
|
||||
status: _statusForString(controller.settingsController.vaultStatus),
|
||||
capabilities: ['KV', 'Namespace', 'Health'],
|
||||
),
|
||||
const _ProviderCardData(
|
||||
name: 'Environment Variables',
|
||||
description: 'Read-only secure provider for local bridge tools.',
|
||||
status: StatusInfo('Available', StatusTone.neutral),
|
||||
_ProviderCardData(
|
||||
name: appText('环境变量', 'Environment Variables'),
|
||||
description: appText(
|
||||
'面向本地桥接工具的只读安全提供方。',
|
||||
'Read-only secure provider for local bridge tools.',
|
||||
),
|
||||
status: StatusInfo(appText('可用', 'Available'), StatusTone.neutral),
|
||||
capabilities: ['Read env', 'Mask refs'],
|
||||
),
|
||||
const _ProviderCardData(
|
||||
name: 'Local Store',
|
||||
description: 'OS-backed secure storage for local secrets and tokens.',
|
||||
status: StatusInfo('Enabled', StatusTone.success),
|
||||
_ProviderCardData(
|
||||
name: appText('本地存储', 'Local Store'),
|
||||
description: appText(
|
||||
'使用系统安全存储保存本地密钥和令牌。',
|
||||
'OS-backed secure storage for local secrets and tokens.',
|
||||
),
|
||||
status: StatusInfo(appText('已启用', 'Enabled'), StatusTone.success),
|
||||
capabilities: ['Local refs', 'Masking'],
|
||||
),
|
||||
const _ProviderCardData(
|
||||
name: 'External Secret Manager',
|
||||
description: 'Reserved adapter surface for external secret services.',
|
||||
status: StatusInfo('Preview', StatusTone.accent),
|
||||
_ProviderCardData(
|
||||
name: appText('外部密钥管理器', 'External Secret Manager'),
|
||||
description: appText(
|
||||
'为外部密钥服务预留的适配器入口。',
|
||||
'Reserved adapter surface for external secret services.',
|
||||
),
|
||||
status: StatusInfo(appText('预览', 'Preview'), StatusTone.accent),
|
||||
capabilities: ['Reserved', 'Extensible'],
|
||||
),
|
||||
];
|
||||
@ -334,19 +354,22 @@ class _ProvidersPanel extends StatelessWidget {
|
||||
onTap: () => onOpenDetail(
|
||||
DetailPanelData(
|
||||
title: provider.name,
|
||||
subtitle: 'Secret Provider',
|
||||
subtitle: appText('密钥提供方', 'Secret Provider'),
|
||||
icon: Icons.key_rounded,
|
||||
status: provider.status,
|
||||
description: provider.description,
|
||||
meta: provider.capabilities,
|
||||
actions: const ['Connect', 'Configure'],
|
||||
actions: [
|
||||
appText('连接', 'Connect'),
|
||||
appText('配置', 'Configure'),
|
||||
],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Capabilities',
|
||||
title: appText('能力', 'Capabilities'),
|
||||
items: provider.capabilities
|
||||
.map(
|
||||
(item) => DetailItem(
|
||||
label: 'Capability',
|
||||
label: appText('能力项', 'Capability'),
|
||||
value: item,
|
||||
),
|
||||
)
|
||||
@ -392,10 +415,7 @@ class _ProvidersPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _AuditPanel extends StatelessWidget {
|
||||
const _AuditPanel({
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
const _AuditPanel({required this.controller, required this.onOpenDetail});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
@ -419,15 +439,24 @@ class _AuditPanel extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
OutlinedButton(onPressed: () {}, child: const Text('状态过滤')),
|
||||
OutlinedButton(onPressed: () {}, child: const Text('时间过滤')),
|
||||
OutlinedButton(
|
||||
onPressed: () {},
|
||||
child: Text(appText('状态过滤', 'Filter Status')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {},
|
||||
child: Text(appText('时间过滤', 'Filter Time')),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (items.isEmpty)
|
||||
SurfaceCard(
|
||||
child: Text(
|
||||
'还没有安全审计条目。保存 Gateway / Vault / Ollama secret 时会在这里出现记录。',
|
||||
appText(
|
||||
'还没有安全审计条目。保存 Gateway、Vault 或 Ollama 密钥后会在这里出现记录。',
|
||||
'No audit entries yet. Records will appear after saving Gateway, Vault, or Ollama secrets.',
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
@ -439,20 +468,32 @@ class _AuditPanel extends StatelessWidget {
|
||||
onTap: () => onOpenDetail(
|
||||
DetailPanelData(
|
||||
title: entry.action,
|
||||
subtitle: 'Audit Entry',
|
||||
subtitle: appText('审计记录', 'Audit Entry'),
|
||||
icon: Icons.policy_outlined,
|
||||
status: _statusForString(entry.status),
|
||||
description: '${entry.provider} · ${entry.target}',
|
||||
meta: [entry.timeLabel, entry.module],
|
||||
actions: const ['View'],
|
||||
actions: [appText('查看', 'View')],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Audit',
|
||||
title: appText('审计', 'Audit'),
|
||||
items: [
|
||||
DetailItem(label: 'Provider', value: entry.provider),
|
||||
DetailItem(label: 'Target', value: entry.target),
|
||||
DetailItem(label: 'Module', value: entry.module),
|
||||
DetailItem(label: 'Status', value: entry.status),
|
||||
DetailItem(
|
||||
label: appText('提供方', 'Provider'),
|
||||
value: entry.provider,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('目标', 'Target'),
|
||||
value: entry.target,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('模块', 'Module'),
|
||||
value: entry.module,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('状态', 'Status'),
|
||||
value: _statusForString(entry.status).label,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -487,10 +528,7 @@ class _AuditPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _SecretRefsTable extends StatelessWidget {
|
||||
const _SecretRefsTable({
|
||||
required this.entries,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
const _SecretRefsTable({required this.entries, required this.onOpenDetail});
|
||||
|
||||
final List<SecretReferenceEntry> entries;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
@ -498,8 +536,10 @@ class _SecretRefsTable extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (entries.isEmpty) {
|
||||
return const SurfaceCard(
|
||||
child: Text('No secret references available yet.'),
|
||||
return SurfaceCard(
|
||||
child: Text(
|
||||
appText('暂时还没有密钥引用。', 'No secret references available yet.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SurfaceCard(
|
||||
@ -510,20 +550,35 @@ class _SecretRefsTable extends StatelessWidget {
|
||||
onTap: () => onOpenDetail(
|
||||
DetailPanelData(
|
||||
title: reference.name,
|
||||
subtitle: 'Secret Reference',
|
||||
subtitle: appText('密钥引用', 'Secret Reference'),
|
||||
icon: Icons.key_rounded,
|
||||
status: _statusForString(reference.status),
|
||||
description: reference.maskedValue,
|
||||
meta: [reference.provider, reference.module],
|
||||
actions: const ['Reveal Ref', 'Open Settings'],
|
||||
actions: [
|
||||
appText('查看引用', 'Reveal Ref'),
|
||||
appText('打开设置', 'Open Settings'),
|
||||
],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Reference',
|
||||
title: appText('引用', 'Reference'),
|
||||
items: [
|
||||
DetailItem(label: 'Provider', value: reference.provider),
|
||||
DetailItem(label: 'Module', value: reference.module),
|
||||
DetailItem(label: 'Masked value', value: reference.maskedValue),
|
||||
DetailItem(label: 'Status', value: reference.status),
|
||||
DetailItem(
|
||||
label: appText('提供方', 'Provider'),
|
||||
value: reference.provider,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('模块', 'Module'),
|
||||
value: reference.module,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('脱敏值', 'Masked value'),
|
||||
value: reference.maskedValue,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('状态', 'Status'),
|
||||
value: _statusForString(reference.status).label,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -543,7 +598,10 @@ class _SecretRefsTable extends StatelessWidget {
|
||||
Expanded(flex: 2, child: Text(reference.provider)),
|
||||
Expanded(flex: 2, child: Text(reference.module)),
|
||||
Expanded(flex: 2, child: Text(reference.maskedValue)),
|
||||
StatusBadge(status: _statusForString(reference.status), compact: true),
|
||||
StatusBadge(
|
||||
status: _statusForString(reference.status),
|
||||
compact: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -570,14 +628,16 @@ class _ProviderCardData {
|
||||
|
||||
StatusInfo _statusForString(String raw) {
|
||||
final value = raw.trim().toLowerCase();
|
||||
if (value.contains('connected') || value.contains('enabled') || value.contains('success')) {
|
||||
return const StatusInfo('Connected', StatusTone.success);
|
||||
if (value.contains('connected') ||
|
||||
value.contains('enabled') ||
|
||||
value.contains('success')) {
|
||||
return StatusInfo(appText('已连接', 'Connected'), StatusTone.success);
|
||||
}
|
||||
if (value.contains('fail') || value.contains('error')) {
|
||||
return const StatusInfo('Error', StatusTone.danger);
|
||||
return StatusInfo(appText('错误', 'Error'), StatusTone.danger);
|
||||
}
|
||||
if (value.contains('preview') || value.contains('reachable')) {
|
||||
return const StatusInfo('Preview', StatusTone.accent);
|
||||
return StatusInfo(appText('预览', 'Preview'), StatusTone.accent);
|
||||
}
|
||||
return const StatusInfo('Idle', StatusTone.neutral);
|
||||
return StatusInfo(appText('空闲', 'Idle'), StatusTone.neutral);
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../runtime/runtime_controllers.dart';
|
||||
import '../../runtime/runtime_models.dart';
|
||||
import '../../widgets/gateway_connect_dialog.dart';
|
||||
@ -10,10 +12,7 @@ import '../../widgets/surface_card.dart';
|
||||
import '../../widgets/top_bar.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
const SettingsPage({super.key, required this.controller});
|
||||
|
||||
final AppController controller;
|
||||
|
||||
@ -22,7 +21,7 @@ class SettingsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
String _tab = 'General';
|
||||
SettingsTab _tab = SettingsTab.general;
|
||||
late final TextEditingController _apisixYamlController;
|
||||
late final TextEditingController _vaultTokenController;
|
||||
late final TextEditingController _ollamaApiKeyController;
|
||||
@ -58,13 +57,16 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
title: 'Settings',
|
||||
subtitle: '配置 $kProductBrandName 工作区、网关默认项、界面与诊断选项',
|
||||
title: appText('设置', 'Settings'),
|
||||
subtitle: appText(
|
||||
'配置 $kProductBrandName 工作区、网关默认项、界面与诊断选项',
|
||||
'Configure workspace, gateway defaults, appearance, and diagnostics for $kProductBrandName.',
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 220,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '搜索',
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索设置', 'Search settings'),
|
||||
prefixIcon: Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
@ -72,28 +74,42 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SectionTabs(
|
||||
items: const [
|
||||
'General',
|
||||
'Workspace',
|
||||
'Gateway',
|
||||
'Appearance',
|
||||
'Diagnostics',
|
||||
'Experimental',
|
||||
'About',
|
||||
],
|
||||
value: _tab,
|
||||
onChanged: (value) => setState(() => _tab = value),
|
||||
items: SettingsTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (value) => setState(
|
||||
() => _tab = SettingsTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
...switch (_tab) {
|
||||
'General' => _buildGeneral(context, controller, settings),
|
||||
'Workspace' => _buildWorkspace(context, controller, settings),
|
||||
'Gateway' => _buildGateway(context, controller, settings),
|
||||
'Appearance' => _buildAppearance(context, controller),
|
||||
'Diagnostics' => _buildDiagnostics(context, controller),
|
||||
'Experimental' => _buildExperimental(context, controller, settings),
|
||||
'About' => _buildAbout(context, controller),
|
||||
_ => const <Widget>[],
|
||||
SettingsTab.general => _buildGeneral(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.workspace => _buildWorkspace(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.gateway => _buildGateway(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.appearance => _buildAppearance(context, controller),
|
||||
SettingsTab.diagnostics => _buildDiagnostics(
|
||||
context,
|
||||
controller,
|
||||
),
|
||||
SettingsTab.experimental => _buildExperimental(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
),
|
||||
SettingsTab.about => _buildAbout(context, controller),
|
||||
},
|
||||
],
|
||||
),
|
||||
@ -115,7 +131,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
Text('Application', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 16),
|
||||
_SwitchRow(
|
||||
label: 'Active workspace shell',
|
||||
label: appText('启用工作台外壳', 'Active workspace shell'),
|
||||
value: settings.appActive,
|
||||
onChanged: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -123,7 +139,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_SwitchRow(
|
||||
label: 'Launch at login',
|
||||
label: appText('开机启动', 'Launch at login'),
|
||||
value: settings.launchAtLogin,
|
||||
onChanged: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -131,7 +147,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_SwitchRow(
|
||||
label: 'Show dock icon',
|
||||
label: appText('显示 Dock 图标', 'Show dock icon'),
|
||||
value: settings.showDockIcon,
|
||||
onChanged: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -139,7 +155,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_SwitchRow(
|
||||
label: 'Account local mode',
|
||||
label: appText('账号本地模式', 'Account local mode'),
|
||||
value: settings.accountLocalMode,
|
||||
onChanged: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -154,10 +170,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Account Access', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('账号访问', 'Account Access'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EditableField(
|
||||
label: 'Account Base URL',
|
||||
label: appText('账号服务地址', 'Account Base URL'),
|
||||
value: settings.accountBaseUrl,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -165,7 +184,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Account Username',
|
||||
label: appText('账号用户名', 'Account Username'),
|
||||
value: settings.accountUsername,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -173,7 +192,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Workspace Label',
|
||||
label: appText('工作区名称', 'Workspace Label'),
|
||||
value: settings.accountWorkspace,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -196,10 +215,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Workspace', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('工作区', 'Workspace'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EditableField(
|
||||
label: 'Workspace Path',
|
||||
label: appText('工作区路径', 'Workspace Path'),
|
||||
value: settings.workspacePath,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -207,7 +229,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Remote Project Root',
|
||||
label: appText('远程项目根目录', 'Remote Project Root'),
|
||||
value: settings.remoteProjectRoot,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -215,15 +237,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'CLI Path',
|
||||
label: appText('CLI 路径', 'CLI Path'),
|
||||
value: settings.cliPath,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(cliPath: value),
|
||||
),
|
||||
onSubmitted: (value) =>
|
||||
_saveSettings(controller, settings.copyWith(cliPath: value)),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Default Model',
|
||||
label: appText('默认模型', 'Default Model'),
|
||||
value: settings.defaultModel,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -231,7 +251,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Default Provider',
|
||||
label: appText('默认提供方', 'Default Provider'),
|
||||
value: settings.defaultProvider,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -246,10 +266,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Ollama Local', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('本地 Ollama', 'Ollama Local'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EditableField(
|
||||
label: 'Endpoint',
|
||||
label: appText('服务地址', 'Endpoint'),
|
||||
value: settings.ollamaLocal.endpoint,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -259,22 +282,26 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Default Model',
|
||||
label: appText('默认模型', 'Default Model'),
|
||||
value: settings.ollamaLocal.defaultModel,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(
|
||||
ollamaLocal: settings.ollamaLocal.copyWith(defaultModel: value),
|
||||
ollamaLocal: settings.ollamaLocal.copyWith(
|
||||
defaultModel: value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_SwitchRow(
|
||||
label: 'Auto Discover',
|
||||
label: appText('自动发现', 'Auto Discover'),
|
||||
value: settings.ollamaLocal.autoDiscover,
|
||||
onChanged: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(
|
||||
ollamaLocal: settings.ollamaLocal.copyWith(autoDiscover: value),
|
||||
ollamaLocal: settings.ollamaLocal.copyWith(
|
||||
autoDiscover: value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -283,7 +310,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => controller.testOllamaConnection(cloud: false),
|
||||
child: Text('Test Connection · ${controller.settingsController.ollamaStatus}'),
|
||||
child: Text(
|
||||
'${appText('测试连接', 'Test Connection')} · ${controller.settingsController.ollamaStatus}',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -294,10 +323,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Ollama Cloud', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('云端 Ollama', 'Ollama Cloud'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EditableField(
|
||||
label: 'Base URL',
|
||||
label: appText('基础地址', 'Base URL'),
|
||||
value: settings.ollamaCloud.baseUrl,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -307,7 +339,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Workspace / Org',
|
||||
label: appText('工作区 / 组织', 'Workspace / Org'),
|
||||
value:
|
||||
'${settings.ollamaCloud.organization} / ${settings.ollamaCloud.workspace}',
|
||||
onSubmitted: (value) {
|
||||
@ -324,12 +356,14 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
},
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Default Model',
|
||||
label: appText('默认模型', 'Default Model'),
|
||||
value: settings.ollamaCloud.defaultModel,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(
|
||||
ollamaCloud: settings.ollamaCloud.copyWith(defaultModel: value),
|
||||
ollamaCloud: settings.ollamaCloud.copyWith(
|
||||
defaultModel: value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -337,7 +371,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
controller: _ollamaApiKeyController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'API Key (${settings.ollamaCloud.apiKeyRef})',
|
||||
labelText:
|
||||
'${appText('API Key', 'API Key')} (${settings.ollamaCloud.apiKeyRef})',
|
||||
),
|
||||
onSubmitted: controller.settingsController.saveOllamaCloudApiKey,
|
||||
),
|
||||
@ -346,7 +381,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => controller.testOllamaConnection(cloud: true),
|
||||
child: Text('Test Cloud · ${controller.settingsController.ollamaStatus}'),
|
||||
child: Text(
|
||||
'${appText('测试云端', 'Test Cloud')} · ${controller.settingsController.ollamaStatus}',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -365,7 +402,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Gateway Connection', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('网关连接', 'Gateway Connection'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'${controller.connection.status.label} · ${controller.connection.remoteAddress ?? settings.gateway.host}:${settings.gateway.port}',
|
||||
@ -384,11 +424,11 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
onDone: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
child: const Text('Open Connect Panel'),
|
||||
child: Text(appText('打开连接面板', 'Open Connect Panel')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: controller.refreshGatewayHealth,
|
||||
child: const Text('Refresh Health'),
|
||||
child: Text(appText('刷新健康状态', 'Refresh Health')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -397,9 +437,14 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
initialValue: controller.selectedAgentId.isEmpty
|
||||
? ''
|
||||
: controller.selectedAgentId,
|
||||
decoration: const InputDecoration(labelText: 'Selected Agent'),
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('当前代理', 'Selected Agent'),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<String>(value: '', child: Text('Main')),
|
||||
DropdownMenuItem<String>(
|
||||
value: '',
|
||||
child: Text(appText('主代理', 'Main')),
|
||||
),
|
||||
...controller.agents.map(
|
||||
(agent) => DropdownMenuItem<String>(
|
||||
value: agent.id,
|
||||
@ -417,18 +462,23 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Vault Server', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('Vault 服务', 'Vault Server'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EditableField(
|
||||
label: 'Address',
|
||||
label: appText('地址', 'Address'),
|
||||
value: settings.vault.address,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(vault: settings.vault.copyWith(address: value)),
|
||||
settings.copyWith(
|
||||
vault: settings.vault.copyWith(address: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Namespace',
|
||||
label: appText('命名空间', 'Namespace'),
|
||||
value: settings.vault.namespace,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -438,26 +488,31 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Auth Mode',
|
||||
label: appText('认证模式', 'Auth Mode'),
|
||||
value: settings.vault.authMode,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(vault: settings.vault.copyWith(authMode: value)),
|
||||
settings.copyWith(
|
||||
vault: settings.vault.copyWith(authMode: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Token Ref',
|
||||
label: appText('Token 引用', 'Token Ref'),
|
||||
value: settings.vault.tokenRef,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(vault: settings.vault.copyWith(tokenRef: value)),
|
||||
settings.copyWith(
|
||||
vault: settings.vault.copyWith(tokenRef: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _vaultTokenController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Vault Token (${settings.vault.tokenRef})',
|
||||
labelText:
|
||||
'${appText('Vault Token', 'Vault Token')} (${settings.vault.tokenRef})',
|
||||
),
|
||||
onSubmitted: controller.settingsController.saveVaultToken,
|
||||
),
|
||||
@ -466,7 +521,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: OutlinedButton(
|
||||
onPressed: controller.testVaultConnection,
|
||||
child: Text('Test Vault · ${controller.settingsController.vaultStatus}'),
|
||||
child: Text(
|
||||
'${appText('测试 Vault', 'Test Vault')} · ${controller.settingsController.vaultStatus}',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -477,18 +534,23 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('APISIX YAML', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('APISIX YAML', 'APISIX YAML'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EditableField(
|
||||
label: 'Profile Name',
|
||||
label: appText('配置名称', 'Profile Name'),
|
||||
value: settings.apisix.name,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(apisix: settings.apisix.copyWith(name: value)),
|
||||
settings.copyWith(
|
||||
apisix: settings.apisix.copyWith(name: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'Source Type',
|
||||
label: appText('来源类型', 'Source Type'),
|
||||
value: settings.apisix.sourceType,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -498,20 +560,25 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_EditableField(
|
||||
label: 'File Path',
|
||||
label: appText('文件路径', 'File Path'),
|
||||
value: settings.apisix.filePath,
|
||||
onSubmitted: (value) => _saveSettings(
|
||||
controller,
|
||||
settings.copyWith(apisix: settings.apisix.copyWith(filePath: value)),
|
||||
settings.copyWith(
|
||||
apisix: settings.apisix.copyWith(filePath: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _apisixYamlController,
|
||||
minLines: 6,
|
||||
maxLines: 10,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Inline YAML',
|
||||
hintText: 'Paste APISIX route / upstream YAML for validation',
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('内联 YAML', 'Inline YAML'),
|
||||
hintText: appText(
|
||||
'粘贴 APISIX 路由或 upstream YAML 用于校验',
|
||||
'Paste APISIX route / upstream YAML for validation',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@ -528,7 +595,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Save Draft'),
|
||||
child: Text(appText('保存草稿', 'Save Draft')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
@ -540,10 +607,12 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
messenger.showSnackBar(SnackBar(content: Text(result.validationMessage)));
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(result.validationMessage)),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'Validate · ${settings.apisix.validationState}',
|
||||
'${appText('校验', 'Validate')} · ${settings.apisix.validationState}',
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -568,24 +637,27 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Theme', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('主题', 'Theme'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ChoiceChip(
|
||||
label: const Text('Light'),
|
||||
label: Text(appText('浅色', 'Light')),
|
||||
selected: controller.themeMode == ThemeMode.light,
|
||||
onSelected: (_) => controller.setThemeMode(ThemeMode.light),
|
||||
),
|
||||
ChoiceChip(
|
||||
label: const Text('Dark'),
|
||||
label: Text(appText('深色', 'Dark')),
|
||||
selected: controller.themeMode == ThemeMode.dark,
|
||||
onSelected: (_) => controller.setThemeMode(ThemeMode.dark),
|
||||
),
|
||||
ChoiceChip(
|
||||
label: const Text('System'),
|
||||
label: Text(appText('跟随系统', 'System')),
|
||||
selected: controller.themeMode == ThemeMode.system,
|
||||
onSelected: (_) => controller.setThemeMode(ThemeMode.system),
|
||||
),
|
||||
@ -606,24 +678,35 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Gateway Diagnostics', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 16),
|
||||
_InfoRow(label: 'Connection', value: controller.connection.status.label),
|
||||
_InfoRow(
|
||||
label: 'Address',
|
||||
value: controller.connection.remoteAddress ?? 'Offline',
|
||||
Text(
|
||||
appText('网关诊断', 'Gateway Diagnostics'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
_InfoRow(label: 'Agent', value: controller.activeAgentName),
|
||||
const SizedBox(height: 16),
|
||||
_InfoRow(
|
||||
label: 'Health Payload',
|
||||
label: appText('连接', 'Connection'),
|
||||
value: controller.connection.status.label,
|
||||
),
|
||||
_InfoRow(
|
||||
label: appText('地址', 'Address'),
|
||||
value:
|
||||
controller.connection.remoteAddress ??
|
||||
appText('离线', 'Offline'),
|
||||
),
|
||||
_InfoRow(
|
||||
label: appText('代理', 'Agent'),
|
||||
value: controller.activeAgentName,
|
||||
),
|
||||
_InfoRow(
|
||||
label: appText('健康负载', 'Health Payload'),
|
||||
value: controller.connection.healthPayload == null
|
||||
? 'Unavailable'
|
||||
? appText('不可用', 'Unavailable')
|
||||
: encodePrettyJson(controller.connection.healthPayload!),
|
||||
),
|
||||
_InfoRow(
|
||||
label: 'Status Payload',
|
||||
label: appText('状态负载', 'Status Payload'),
|
||||
value: controller.connection.statusPayload == null
|
||||
? 'Unavailable'
|
||||
? appText('不可用', 'Unavailable')
|
||||
: encodePrettyJson(controller.connection.statusPayload!),
|
||||
),
|
||||
],
|
||||
@ -634,12 +717,21 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Device', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('设备', 'Device'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_InfoRow(label: 'Platform', value: controller.runtime.deviceInfo.platformLabel),
|
||||
_InfoRow(label: 'Device Family', value: controller.runtime.deviceInfo.deviceFamily),
|
||||
_InfoRow(
|
||||
label: 'Model Identifier',
|
||||
label: appText('平台', 'Platform'),
|
||||
value: controller.runtime.deviceInfo.platformLabel,
|
||||
),
|
||||
_InfoRow(
|
||||
label: appText('设备类型', 'Device Family'),
|
||||
value: controller.runtime.deviceInfo.deviceFamily,
|
||||
),
|
||||
_InfoRow(
|
||||
label: appText('型号标识', 'Model Identifier'),
|
||||
value: controller.runtime.deviceInfo.modelIdentifier,
|
||||
),
|
||||
],
|
||||
@ -658,10 +750,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Experimental', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('实验特性', 'Experimental'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_SwitchRow(
|
||||
label: 'Canvas host',
|
||||
label: appText('Canvas 宿主', 'Canvas host'),
|
||||
value: settings.experimentalCanvas,
|
||||
onChanged: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -669,7 +764,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_SwitchRow(
|
||||
label: 'Bridge mode',
|
||||
label: appText('桥接模式', 'Bridge mode'),
|
||||
value: settings.experimentalBridge,
|
||||
onChanged: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -677,7 +772,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
),
|
||||
),
|
||||
_SwitchRow(
|
||||
label: 'Debug runtime',
|
||||
label: appText('调试运行时', 'Debug runtime'),
|
||||
value: settings.experimentalDebug,
|
||||
onChanged: (value) => _saveSettings(
|
||||
controller,
|
||||
@ -690,21 +785,30 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildAbout(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
) {
|
||||
List<Widget> _buildAbout(BuildContext context, AppController controller) {
|
||||
return [
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('About', style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
appText('关于', 'About'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_InfoRow(label: 'App', value: kSystemAppName),
|
||||
_InfoRow(label: 'Version', value: controller.runtime.packageInfo.version),
|
||||
_InfoRow(label: 'Build', value: controller.runtime.packageInfo.buildNumber),
|
||||
_InfoRow(label: 'Package', value: controller.runtime.packageInfo.packageName),
|
||||
_InfoRow(label: appText('应用', 'App'), value: kSystemAppName),
|
||||
_InfoRow(
|
||||
label: appText('版本', 'Version'),
|
||||
value: controller.runtime.packageInfo.version,
|
||||
),
|
||||
_InfoRow(
|
||||
label: appText('构建号', 'Build'),
|
||||
value: controller.runtime.packageInfo.buildNumber,
|
||||
),
|
||||
_InfoRow(
|
||||
label: appText('包名', 'Package'),
|
||||
value: controller.runtime.packageInfo.packageName,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -767,10 +871,7 @@ class _SwitchRow extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
const _InfoRow({
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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 '../../widgets/metric_card.dart';
|
||||
@ -24,37 +25,37 @@ class TasksPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TasksPageState extends State<TasksPage> {
|
||||
String _tab = 'Queue';
|
||||
TasksTab _tab = TasksTab.queue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = widget.controller;
|
||||
final items = controller.taskItemsForTab(_tab);
|
||||
final items = controller.taskItemsForTab(_tabKey);
|
||||
final metrics = [
|
||||
MetricSummary(
|
||||
label: 'Total',
|
||||
label: appText('总数', 'Total'),
|
||||
value: '${controller.tasksController.totalCount}',
|
||||
caption: '从 sessions / chat 派生',
|
||||
caption: appText('从会话与对话中派生', 'Derived from sessions / chat'),
|
||||
icon: Icons.layers_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Running',
|
||||
label: appText('运行中', 'Running'),
|
||||
value: '${controller.tasksController.running.length}',
|
||||
caption: '当前活跃 run',
|
||||
caption: appText('当前活跃运行', 'Current active runs'),
|
||||
icon: Icons.play_circle_outline_rounded,
|
||||
status: _statusInfoForTask('Running'),
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Failed',
|
||||
label: appText('失败', 'Failed'),
|
||||
value: '${controller.tasksController.failed.length}',
|
||||
caption: 'aborted / error run',
|
||||
caption: appText('中断或报错的运行', 'Aborted / error runs'),
|
||||
icon: Icons.error_outline_rounded,
|
||||
status: _statusInfoForTask('Failed'),
|
||||
),
|
||||
MetricSummary(
|
||||
label: 'Scheduled',
|
||||
label: appText('计划中', 'Scheduled'),
|
||||
value: '${controller.tasksController.scheduled.length}',
|
||||
caption: '等待自动化管理包接入',
|
||||
caption: appText('等待自动化能力接入', 'Pending automation integration'),
|
||||
icon: Icons.event_repeat_rounded,
|
||||
),
|
||||
];
|
||||
@ -68,8 +69,11 @@ class _TasksPageState extends State<TasksPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
title: 'Tasks',
|
||||
subtitle: '查看任务队列、执行状态与历史记录',
|
||||
title: appText('任务', 'Tasks'),
|
||||
subtitle: appText(
|
||||
'查看任务队列、执行状态与历史记录',
|
||||
'Review queue, execution state, and history.',
|
||||
),
|
||||
trailing: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
@ -78,8 +82,8 @@ class _TasksPageState extends State<TasksPage> {
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '搜索',
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索任务', 'Search tasks'),
|
||||
prefixIcon: Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
@ -89,20 +93,23 @@ class _TasksPageState extends State<TasksPage> {
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => controller.navigateTo(
|
||||
WorkspaceDestination.assistant,
|
||||
),
|
||||
onPressed: () =>
|
||||
controller.navigateTo(WorkspaceDestination.assistant),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('新建'),
|
||||
label: Text(appText('新建任务', 'New Task')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SectionTabs(
|
||||
items: const ['Queue', 'Running', 'History', 'Failed', 'Scheduled'],
|
||||
value: _tab,
|
||||
onChanged: (value) => setState(() => _tab = value),
|
||||
items: TasksTab.values.map((item) => item.label).toList(),
|
||||
value: _tab.label,
|
||||
onChanged: (value) => setState(
|
||||
() => _tab = TasksTab.values.firstWhere(
|
||||
(item) => item.label == value,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
LayoutBuilder(
|
||||
@ -127,19 +134,26 @@ class _TasksPageState extends State<TasksPage> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_tab == 'Scheduled' && items.isEmpty)
|
||||
if (_tab == TasksTab.scheduled && items.isEmpty)
|
||||
SurfaceCard(
|
||||
child: Text(
|
||||
'Scheduled 任务将在自动化管理包接入后展示。本轮只显示来自 Gateway sessions / chat 的派生任务。',
|
||||
appText(
|
||||
'计划任务会在自动化能力接入后展示。当前仅显示来自 Gateway 会话与对话的派生任务。',
|
||||
'Scheduled tasks will appear after automation is integrated. Only session/chat-derived tasks are shown for now.',
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
)
|
||||
else if (items.isEmpty)
|
||||
SurfaceCard(
|
||||
child: Text(
|
||||
controller.connection.status == RuntimeConnectionStatus.connected
|
||||
? '当前 tab 暂无任务。'
|
||||
: '连接 Gateway 后,这里会显示真实的 queue / running / history / failed 视图。',
|
||||
controller.connection.status ==
|
||||
RuntimeConnectionStatus.connected
|
||||
? appText('当前页签暂无任务。', 'No tasks in this tab.')
|
||||
: appText(
|
||||
'连接 Gateway 后,这里会显示真实的队列、运行中、历史和失败任务。',
|
||||
'Connect a gateway to load live queue, running, history, and failed tasks.',
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
)
|
||||
@ -157,7 +171,9 @@ class _TasksPageState extends State<TasksPage> {
|
||||
children: [
|
||||
Text(
|
||||
task.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
@ -191,12 +207,16 @@ class _TasksPageState extends State<TasksPage> {
|
||||
children: [
|
||||
Text(
|
||||
task.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
task.summary,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -211,7 +231,10 @@ class _TasksPageState extends State<TasksPage> {
|
||||
),
|
||||
),
|
||||
Expanded(flex: 2, child: Text(task.owner)),
|
||||
Expanded(flex: 2, child: Text(task.startedAtLabel)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(task.startedAtLabel),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded),
|
||||
],
|
||||
);
|
||||
@ -224,7 +247,10 @@ class _TasksPageState extends State<TasksPage> {
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'点击任务项后弹出 Detail Drawer',
|
||||
appText(
|
||||
'点击任务项后会打开详情侧栏',
|
||||
'Click a task to open the detail drawer.',
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
@ -238,32 +264,54 @@ class _TasksPageState extends State<TasksPage> {
|
||||
DetailPanelData _taskDetail(DerivedTaskItem task) {
|
||||
return DetailPanelData(
|
||||
title: task.title,
|
||||
subtitle: 'Session-derived Task',
|
||||
subtitle: appText('会话派生任务', 'Session-derived Task'),
|
||||
icon: Icons.layers_rounded,
|
||||
status: _statusInfoForTask(task.status),
|
||||
description: task.summary,
|
||||
meta: [task.surface, task.sessionKey],
|
||||
actions: const ['Open Session', 'Refresh'],
|
||||
actions: [appText('打开会话', 'Open Session'), appText('刷新', 'Refresh')],
|
||||
sections: [
|
||||
DetailSection(
|
||||
title: 'Task',
|
||||
title: appText('任务', 'Task'),
|
||||
items: [
|
||||
DetailItem(label: 'Owner', value: task.owner),
|
||||
DetailItem(label: 'Status', value: task.status),
|
||||
DetailItem(label: 'Started', value: task.startedAtLabel),
|
||||
DetailItem(label: 'Updated', value: task.durationLabel),
|
||||
DetailItem(label: 'Session Key', value: task.sessionKey),
|
||||
DetailItem(label: appText('负责人', 'Owner'), value: task.owner),
|
||||
DetailItem(
|
||||
label: appText('状态', 'Status'),
|
||||
value: _statusLabel(task.status),
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('开始时间', 'Started'),
|
||||
value: task.startedAtLabel,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('更新时间', 'Updated'),
|
||||
value: task.durationLabel,
|
||||
),
|
||||
DetailItem(
|
||||
label: appText('会话 Key', 'Session Key'),
|
||||
value: task.sessionKey,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String get _tabKey => switch (_tab) {
|
||||
TasksTab.queue => 'Queue',
|
||||
TasksTab.running => 'Running',
|
||||
TasksTab.history => 'History',
|
||||
TasksTab.failed => 'Failed',
|
||||
TasksTab.scheduled => 'Scheduled',
|
||||
};
|
||||
}
|
||||
|
||||
StatusInfo _statusInfoForTask(String status) => switch (status) {
|
||||
'Running' => const StatusInfo('Running', StatusTone.accent),
|
||||
'Failed' => const StatusInfo('Failed', StatusTone.danger),
|
||||
'Queued' => const StatusInfo('Queued', StatusTone.neutral),
|
||||
'Scheduled' => const StatusInfo('Scheduled', StatusTone.accent),
|
||||
_ => const StatusInfo('Completed', StatusTone.success),
|
||||
'Running' => StatusInfo(appText('运行中', 'Running'), StatusTone.accent),
|
||||
'Failed' => StatusInfo(appText('失败', 'Failed'), StatusTone.danger),
|
||||
'Queued' => StatusInfo(appText('排队中', 'Queued'), StatusTone.neutral),
|
||||
'Scheduled' => StatusInfo(appText('计划中', 'Scheduled'), StatusTone.accent),
|
||||
_ => StatusInfo(appText('已完成', 'Completed'), StatusTone.success),
|
||||
};
|
||||
|
||||
String _statusLabel(String status) => _statusInfoForTask(status).label;
|
||||
|
||||
36
lib/i18n/app_language.dart
Normal file
36
lib/i18n/app_language.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum AppLanguage { zh, en }
|
||||
|
||||
extension AppLanguageCopy on AppLanguage {
|
||||
String get code => switch (this) {
|
||||
AppLanguage.zh => 'zh',
|
||||
AppLanguage.en => 'en',
|
||||
};
|
||||
|
||||
String get buttonLabel => switch (this) {
|
||||
AppLanguage.zh => '中 / EN',
|
||||
AppLanguage.en => 'EN / 中',
|
||||
};
|
||||
|
||||
static AppLanguage fromJsonValue(String? value) {
|
||||
return AppLanguage.values.firstWhere(
|
||||
(item) => item.name == value,
|
||||
orElse: () => AppLanguage.zh,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppLanguage _activeAppLanguage = AppLanguage.zh;
|
||||
|
||||
AppLanguage get activeAppLanguage => _activeAppLanguage;
|
||||
|
||||
Locale get activeAppLocale => Locale(_activeAppLanguage.code);
|
||||
|
||||
void setActiveAppLanguage(AppLanguage language) {
|
||||
_activeAppLanguage = language;
|
||||
}
|
||||
|
||||
String appText(String zh, String en) {
|
||||
return _activeAppLanguage == AppLanguage.zh ? zh : en;
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../i18n/app_language.dart';
|
||||
|
||||
enum WorkspaceDestination {
|
||||
assistant,
|
||||
tasks,
|
||||
@ -11,12 +13,12 @@ enum WorkspaceDestination {
|
||||
|
||||
extension WorkspaceDestinationCopy on WorkspaceDestination {
|
||||
String get label => switch (this) {
|
||||
WorkspaceDestination.assistant => 'Assistant',
|
||||
WorkspaceDestination.tasks => 'Tasks',
|
||||
WorkspaceDestination.modules => 'Modules',
|
||||
WorkspaceDestination.secrets => 'Secrets',
|
||||
WorkspaceDestination.settings => 'Settings',
|
||||
WorkspaceDestination.account => 'Account',
|
||||
WorkspaceDestination.assistant => appText('助手', 'Assistant'),
|
||||
WorkspaceDestination.tasks => appText('任务', 'Tasks'),
|
||||
WorkspaceDestination.modules => appText('模块', 'Modules'),
|
||||
WorkspaceDestination.secrets => appText('密钥', 'Secrets'),
|
||||
WorkspaceDestination.settings => appText('设置', 'Settings'),
|
||||
WorkspaceDestination.account => appText('账号', 'Account'),
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
@ -29,13 +31,30 @@ extension WorkspaceDestinationCopy on WorkspaceDestination {
|
||||
};
|
||||
|
||||
String get description => switch (this) {
|
||||
WorkspaceDestination.assistant => 'AI 主入口,优先承接自然输入和高频工作发起。',
|
||||
WorkspaceDestination.tasks => '任务队列、运行态、失败项和调度历史的统一视图。',
|
||||
WorkspaceDestination.modules =>
|
||||
WorkspaceDestination.assistant => appText(
|
||||
'AI 主入口,优先承接自然输入和高频工作发起。',
|
||||
'Primary AI entry point for natural input and frequent task starts.',
|
||||
),
|
||||
WorkspaceDestination.tasks => appText(
|
||||
'任务队列、运行态、失败项和调度历史的统一视图。',
|
||||
'Unified view for queue, running, failed, and history.',
|
||||
),
|
||||
WorkspaceDestination.modules => appText(
|
||||
'平台能力中心,管理 Gateway、Nodes、Agents、Skills 与 Connectors。',
|
||||
WorkspaceDestination.secrets => 'Vault、Provider 凭证与审计信息的轻量管理面。',
|
||||
WorkspaceDestination.settings => '全局配置中心,只负责系统设置与诊断,不承担业务模块入口。',
|
||||
WorkspaceDestination.account => '用户身份、工作区切换与登录会话管理。',
|
||||
'Capability center for gateway, nodes, agents, skills, and connectors.',
|
||||
),
|
||||
WorkspaceDestination.secrets => appText(
|
||||
'Vault、Provider 凭证与审计信息的轻量管理面。',
|
||||
'Lightweight management for vault, provider credentials, and audit data.',
|
||||
),
|
||||
WorkspaceDestination.settings => appText(
|
||||
'全局配置中心,只负责系统设置与诊断,不承担业务模块入口。',
|
||||
'Global settings and diagnostics, separated from business modules.',
|
||||
),
|
||||
WorkspaceDestination.account => appText(
|
||||
'用户身份、工作区切换与登录会话管理。',
|
||||
'Identity, workspace switching, and session management.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -52,8 +71,8 @@ enum AssistantMode { code, office }
|
||||
|
||||
extension AssistantModeCopy on AssistantMode {
|
||||
String get label => switch (this) {
|
||||
AssistantMode.code => '代码开发',
|
||||
AssistantMode.office => '日常办公',
|
||||
AssistantMode.code => appText('代码开发', 'Code'),
|
||||
AssistantMode.office => appText('日常办公', 'Office'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -61,11 +80,11 @@ enum TasksTab { queue, running, history, failed, scheduled }
|
||||
|
||||
extension TasksTabCopy on TasksTab {
|
||||
String get label => switch (this) {
|
||||
TasksTab.queue => 'Queue',
|
||||
TasksTab.running => 'Running',
|
||||
TasksTab.history => 'History',
|
||||
TasksTab.failed => 'Failed',
|
||||
TasksTab.scheduled => 'Scheduled',
|
||||
TasksTab.queue => appText('队列', 'Queue'),
|
||||
TasksTab.running => appText('运行中', 'Running'),
|
||||
TasksTab.history => appText('历史', 'History'),
|
||||
TasksTab.failed => appText('失败', 'Failed'),
|
||||
TasksTab.scheduled => appText('计划中', 'Scheduled'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,12 +92,12 @@ enum ModulesTab { gateway, nodes, agents, skills, clawHub, connectors }
|
||||
|
||||
extension ModulesTabCopy on ModulesTab {
|
||||
String get label => switch (this) {
|
||||
ModulesTab.gateway => 'Gateway',
|
||||
ModulesTab.nodes => 'Nodes',
|
||||
ModulesTab.agents => 'Agents',
|
||||
ModulesTab.skills => 'Skills',
|
||||
ModulesTab.gateway => appText('网关', 'Gateway'),
|
||||
ModulesTab.nodes => appText('节点', 'Nodes'),
|
||||
ModulesTab.agents => appText('代理', 'Agents'),
|
||||
ModulesTab.skills => appText('技能', 'Skills'),
|
||||
ModulesTab.clawHub => 'ClawHub',
|
||||
ModulesTab.connectors => 'Connectors',
|
||||
ModulesTab.connectors => appText('连接器', 'Connectors'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -87,9 +106,9 @@ enum SecretsTab { vault, localStore, providers, audit }
|
||||
extension SecretsTabCopy on SecretsTab {
|
||||
String get label => switch (this) {
|
||||
SecretsTab.vault => 'Vault',
|
||||
SecretsTab.localStore => 'Local Store',
|
||||
SecretsTab.providers => 'Providers',
|
||||
SecretsTab.audit => 'Audit',
|
||||
SecretsTab.localStore => appText('本地存储', 'Local Store'),
|
||||
SecretsTab.providers => appText('提供方', 'Providers'),
|
||||
SecretsTab.audit => appText('审计', 'Audit'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,13 +124,13 @@ enum SettingsTab {
|
||||
|
||||
extension SettingsTabCopy on SettingsTab {
|
||||
String get label => switch (this) {
|
||||
SettingsTab.general => 'General',
|
||||
SettingsTab.workspace => 'Workspace',
|
||||
SettingsTab.gateway => 'Gateway',
|
||||
SettingsTab.appearance => 'Appearance',
|
||||
SettingsTab.diagnostics => 'Diagnostics',
|
||||
SettingsTab.experimental => 'Experimental',
|
||||
SettingsTab.about => 'About',
|
||||
SettingsTab.general => appText('通用', 'General'),
|
||||
SettingsTab.workspace => appText('工作区', 'Workspace'),
|
||||
SettingsTab.gateway => appText('网关', 'Gateway'),
|
||||
SettingsTab.appearance => appText('外观', 'Appearance'),
|
||||
SettingsTab.diagnostics => appText('诊断', 'Diagnostics'),
|
||||
SettingsTab.experimental => appText('实验特性', 'Experimental'),
|
||||
SettingsTab.about => appText('关于', 'About'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -119,9 +138,9 @@ enum AccountTab { profile, workspace, sessions }
|
||||
|
||||
extension AccountTabCopy on AccountTab {
|
||||
String get label => switch (this) {
|
||||
AccountTab.profile => 'Profile',
|
||||
AccountTab.workspace => 'Workspace',
|
||||
AccountTab.sessions => 'Sessions',
|
||||
AccountTab.profile => appText('资料', 'Profile'),
|
||||
AccountTab.workspace => appText('工作区', 'Workspace'),
|
||||
AccountTab.sessions => appText('会话', 'Sessions'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../i18n/app_language.dart';
|
||||
|
||||
enum RuntimeConnectionMode { unconfigured, local, remote }
|
||||
|
||||
extension RuntimeConnectionModeCopy on RuntimeConnectionMode {
|
||||
String get label => switch (this) {
|
||||
RuntimeConnectionMode.unconfigured => 'Unconfigured',
|
||||
RuntimeConnectionMode.local => 'Local',
|
||||
RuntimeConnectionMode.remote => 'Remote',
|
||||
RuntimeConnectionMode.unconfigured => appText('未配置', 'Unconfigured'),
|
||||
RuntimeConnectionMode.local => appText('本地', 'Local'),
|
||||
RuntimeConnectionMode.remote => appText('远程', 'Remote'),
|
||||
};
|
||||
|
||||
static RuntimeConnectionMode fromJsonValue(String? value) {
|
||||
@ -21,10 +23,10 @@ enum RuntimeConnectionStatus { offline, connecting, connected, error }
|
||||
|
||||
extension RuntimeConnectionStatusCopy on RuntimeConnectionStatus {
|
||||
String get label => switch (this) {
|
||||
RuntimeConnectionStatus.offline => 'Offline',
|
||||
RuntimeConnectionStatus.connecting => 'Connecting',
|
||||
RuntimeConnectionStatus.connected => 'Connected',
|
||||
RuntimeConnectionStatus.error => 'Error',
|
||||
RuntimeConnectionStatus.offline => appText('离线', 'Offline'),
|
||||
RuntimeConnectionStatus.connecting => appText('连接中', 'Connecting'),
|
||||
RuntimeConnectionStatus.connected => appText('已连接', 'Connected'),
|
||||
RuntimeConnectionStatus.error => appText('错误', 'Error'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -32,8 +34,8 @@ enum AssistantExecutionTarget { local, remote }
|
||||
|
||||
extension AssistantExecutionTargetCopy on AssistantExecutionTarget {
|
||||
String get label => switch (this) {
|
||||
AssistantExecutionTarget.local => '本地',
|
||||
AssistantExecutionTarget.remote => '远程',
|
||||
AssistantExecutionTarget.local => appText('本地', 'Local'),
|
||||
AssistantExecutionTarget.remote => appText('远程', 'Remote'),
|
||||
};
|
||||
|
||||
String get promptValue => switch (this) {
|
||||
@ -53,8 +55,8 @@ enum AssistantPermissionLevel { defaultAccess, fullAccess }
|
||||
|
||||
extension AssistantPermissionLevelCopy on AssistantPermissionLevel {
|
||||
String get label => switch (this) {
|
||||
AssistantPermissionLevel.defaultAccess => '默认权限',
|
||||
AssistantPermissionLevel.fullAccess => '完全访问权限',
|
||||
AssistantPermissionLevel.defaultAccess => appText('默认权限', 'Default Access'),
|
||||
AssistantPermissionLevel.fullAccess => appText('完全访问权限', 'Full Access'),
|
||||
};
|
||||
|
||||
String get promptValue => switch (this) {
|
||||
@ -398,6 +400,7 @@ class ApisixYamlProfile {
|
||||
|
||||
class SettingsSnapshot {
|
||||
const SettingsSnapshot({
|
||||
required this.appLanguage,
|
||||
required this.appActive,
|
||||
required this.launchAtLogin,
|
||||
required this.showDockIcon,
|
||||
@ -422,6 +425,7 @@ class SettingsSnapshot {
|
||||
required this.assistantPermissionLevel,
|
||||
});
|
||||
|
||||
final AppLanguage appLanguage;
|
||||
final bool appActive;
|
||||
final bool launchAtLogin;
|
||||
final bool showDockIcon;
|
||||
@ -447,6 +451,7 @@ class SettingsSnapshot {
|
||||
|
||||
factory SettingsSnapshot.defaults() {
|
||||
return SettingsSnapshot(
|
||||
appLanguage: AppLanguage.zh,
|
||||
appActive: true,
|
||||
launchAtLogin: false,
|
||||
showDockIcon: true,
|
||||
@ -473,6 +478,7 @@ class SettingsSnapshot {
|
||||
}
|
||||
|
||||
SettingsSnapshot copyWith({
|
||||
AppLanguage? appLanguage,
|
||||
bool? appActive,
|
||||
bool? launchAtLogin,
|
||||
bool? showDockIcon,
|
||||
@ -497,6 +503,7 @@ class SettingsSnapshot {
|
||||
AssistantPermissionLevel? assistantPermissionLevel,
|
||||
}) {
|
||||
return SettingsSnapshot(
|
||||
appLanguage: appLanguage ?? this.appLanguage,
|
||||
appActive: appActive ?? this.appActive,
|
||||
launchAtLogin: launchAtLogin ?? this.launchAtLogin,
|
||||
showDockIcon: showDockIcon ?? this.showDockIcon,
|
||||
@ -526,6 +533,7 @@ class SettingsSnapshot {
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'appLanguage': appLanguage.name,
|
||||
'appActive': appActive,
|
||||
'launchAtLogin': launchAtLogin,
|
||||
'showDockIcon': showDockIcon,
|
||||
@ -553,6 +561,9 @@ class SettingsSnapshot {
|
||||
|
||||
factory SettingsSnapshot.fromJson(Map<String, dynamic> json) {
|
||||
return SettingsSnapshot(
|
||||
appLanguage: AppLanguageCopy.fromJsonValue(
|
||||
json['appLanguage'] as String?,
|
||||
),
|
||||
appActive: json['appActive'] as bool? ?? true,
|
||||
launchAtLogin: json['launchAtLogin'] as bool? ?? false,
|
||||
showDockIcon: json['showDockIcon'] as bool? ?? true,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_controller.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../runtime/runtime_models.dart';
|
||||
import 'section_tabs.dart';
|
||||
|
||||
@ -27,7 +28,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
final TextEditingController _tokenController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
String _mode = 'Setup Code';
|
||||
String _mode = 'setup';
|
||||
bool _tls = true;
|
||||
RuntimeConnectionMode _connectionMode = RuntimeConnectionMode.remote;
|
||||
bool _submitting = false;
|
||||
@ -41,7 +42,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
_portController = TextEditingController(text: '${profile.port}');
|
||||
_tls = profile.tls;
|
||||
_connectionMode = profile.mode;
|
||||
_mode = profile.useSetupCode ? 'Setup Code' : 'Manual';
|
||||
_mode = profile.useSetupCode ? 'setup' : 'manual';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -63,36 +64,53 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Gateway Access', style: theme.textTheme.headlineSmall),
|
||||
Text(
|
||||
appText('Gateway 访问', 'Gateway Access'),
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Connect XWorkmate to an OpenClaw gateway with setup code or manual host / TLS.',
|
||||
appText(
|
||||
'通过配置码或手动 Host / TLS 将 XWorkmate 连接到 OpenClaw Gateway。',
|
||||
'Connect XWorkmate to an OpenClaw gateway with setup code or manual host / TLS.',
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
SectionTabs(
|
||||
items: const ['Setup Code', 'Manual'],
|
||||
value: _mode,
|
||||
items: [appText('配置码', 'Setup Code'), appText('手动配置', 'Manual')],
|
||||
value: _mode == 'setup'
|
||||
? appText('配置码', 'Setup Code')
|
||||
: appText('手动配置', 'Manual'),
|
||||
size: SectionTabsSize.small,
|
||||
onChanged: (value) => setState(() => _mode = value),
|
||||
onChanged: (value) => setState(
|
||||
() => _mode = value == appText('配置码', 'Setup Code')
|
||||
? 'setup'
|
||||
: 'manual',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
_StatusBanner(controller: widget.controller),
|
||||
const SizedBox(height: 18),
|
||||
if (_mode == 'Setup Code') ...[
|
||||
if (_mode == 'setup') ...[
|
||||
TextField(
|
||||
controller: _setupCodeController,
|
||||
minLines: 4,
|
||||
maxLines: 6,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Setup Code',
|
||||
hintText: 'Paste gateway setup code or JSON payload',
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('配置码', 'Setup Code'),
|
||||
hintText: appText(
|
||||
'粘贴 Gateway 配置码或 JSON 负载',
|
||||
'Paste gateway setup code or JSON payload',
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
DropdownButtonFormField<RuntimeConnectionMode>(
|
||||
initialValue: _connectionMode,
|
||||
decoration: const InputDecoration(labelText: 'Connection Mode'),
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('连接模式', 'Connection Mode'),
|
||||
),
|
||||
items: RuntimeConnectionMode.values
|
||||
.map(
|
||||
(mode) => DropdownMenuItem<RuntimeConnectionMode>(
|
||||
@ -118,7 +136,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _hostController,
|
||||
decoration: const InputDecoration(labelText: 'Host'),
|
||||
decoration: InputDecoration(labelText: appText('主机', 'Host')),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
@ -127,7 +145,9 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
child: TextField(
|
||||
controller: _portController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Port'),
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('端口', 'Port'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@ -135,7 +155,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
child: SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _tls,
|
||||
title: const Text('TLS'),
|
||||
title: Text(appText('TLS', 'TLS')),
|
||||
onChanged: _connectionMode == RuntimeConnectionMode.local
|
||||
? null
|
||||
: (value) => setState(() => _tls = value),
|
||||
@ -147,18 +167,21 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
const SizedBox(height: 18),
|
||||
TextField(
|
||||
controller: _tokenController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Shared Token',
|
||||
hintText: 'Optional override for gateway token',
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('共享 Token', 'Shared Token'),
|
||||
hintText: appText(
|
||||
'可选:覆盖默认 Gateway Token',
|
||||
'Optional override for gateway token',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Password',
|
||||
hintText: 'Optional shared password',
|
||||
decoration: InputDecoration(
|
||||
labelText: appText('密码', 'Password'),
|
||||
hintText: appText('可选:共享密码', 'Optional shared password'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@ -180,12 +203,16 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.link_off_rounded),
|
||||
label: const Text('Disconnect'),
|
||||
label: Text(appText('断开连接', 'Disconnect')),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
icon: const Icon(Icons.wifi_tethering_rounded),
|
||||
label: Text(_submitting ? 'Connecting…' : 'Connect'),
|
||||
label: Text(
|
||||
_submitting
|
||||
? appText('连接中…', 'Connecting…')
|
||||
: appText('连接', 'Connect'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -208,7 +235,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
|
||||
Future<void> _submit() async {
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
if (_mode == 'Setup Code') {
|
||||
if (_mode == 'setup') {
|
||||
await widget.controller.connectWithSetupCode(
|
||||
setupCode: _setupCodeController.text,
|
||||
token: _tokenController.text,
|
||||
@ -245,8 +272,10 @@ class _StatusBanner extends StatelessWidget {
|
||||
final tone = switch (connection.status) {
|
||||
RuntimeConnectionStatus.connected => theme.colorScheme.primaryContainer,
|
||||
RuntimeConnectionStatus.error => theme.colorScheme.errorContainer,
|
||||
RuntimeConnectionStatus.connecting => theme.colorScheme.secondaryContainer,
|
||||
RuntimeConnectionStatus.offline => theme.colorScheme.surfaceContainerHighest,
|
||||
RuntimeConnectionStatus.connecting =>
|
||||
theme.colorScheme.secondaryContainer,
|
||||
RuntimeConnectionStatus.offline =>
|
||||
theme.colorScheme.surfaceContainerHighest,
|
||||
};
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@ -258,10 +287,7 @@ class _StatusBanner extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
connection.status.label,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Text(connection.status.label, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
connection.remoteAddress ?? 'No active gateway target',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app/app_metadata.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
|
||||
@ -9,8 +10,10 @@ class SidebarNavigation extends StatelessWidget {
|
||||
super.key,
|
||||
required this.currentSection,
|
||||
required this.isCollapsed,
|
||||
required this.appLanguage,
|
||||
required this.themeMode,
|
||||
required this.onSectionChanged,
|
||||
required this.onToggleLanguage,
|
||||
required this.onToggleCollapsed,
|
||||
required this.onOpenAccount,
|
||||
required this.onOpenThemeToggle,
|
||||
@ -18,8 +21,10 @@ class SidebarNavigation extends StatelessWidget {
|
||||
|
||||
final WorkspaceDestination currentSection;
|
||||
final bool isCollapsed;
|
||||
final AppLanguage appLanguage;
|
||||
final ThemeMode themeMode;
|
||||
final ValueChanged<WorkspaceDestination> onSectionChanged;
|
||||
final VoidCallback onToggleLanguage;
|
||||
final VoidCallback onToggleCollapsed;
|
||||
final VoidCallback onOpenAccount;
|
||||
final VoidCallback onOpenThemeToggle;
|
||||
@ -39,25 +44,27 @@ class SidebarNavigation extends StatelessWidget {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: isCollapsed ? 78 : 252,
|
||||
margin: const EdgeInsets.all(16),
|
||||
width: isCollapsed ? 72 : 236,
|
||||
margin: const EdgeInsets.fromLTRB(8, 8, 6, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.sidebar,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(color: palette.sidebarBorder),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: palette.sidebarBorder.withValues(alpha: 0.72),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isCollapsed ? 10 : 16,
|
||||
vertical: 18,
|
||||
horizontal: isCollapsed ? 8 : 12,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SidebarHeader(isCollapsed: isCollapsed),
|
||||
const SizedBox(height: 18),
|
||||
const SizedBox(height: 12),
|
||||
Container(height: 1, color: palette.sidebarBorder),
|
||||
const SizedBox(height: 18),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@ -68,7 +75,7 @@ class SidebarNavigation extends StatelessWidget {
|
||||
children: _mainSections
|
||||
.map(
|
||||
(section) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: SidebarNavItem(
|
||||
section: section,
|
||||
selected: currentSection == section,
|
||||
@ -81,12 +88,14 @@ class SidebarNavigation extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 8),
|
||||
Container(height: 1, color: palette.sidebarBorder),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 10),
|
||||
SidebarFooter(
|
||||
isCollapsed: isCollapsed,
|
||||
appLanguage: appLanguage,
|
||||
themeMode: themeMode,
|
||||
onToggleLanguage: onToggleLanguage,
|
||||
onOpenThemeToggle: onOpenThemeToggle,
|
||||
onOpenSettings: () =>
|
||||
onSectionChanged(WorkspaceDestination.settings),
|
||||
@ -117,20 +126,20 @@ class SidebarHeader extends StatelessWidget {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: palette.accentMuted,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.auto_awesome_rounded,
|
||||
color: palette.accent,
|
||||
size: 22,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
if (!isCollapsed) ...[
|
||||
const SizedBox(width: 14),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -141,7 +150,7 @@ class SidebarHeader extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
kProductSubtitle,
|
||||
appText('可执行 AI 工作台', kProductSubtitle),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
@ -190,17 +199,17 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
|
||||
curve: Curves.easeOutCubic,
|
||||
decoration: BoxDecoration(
|
||||
color: background,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: widget.onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: widget.collapsed ? 0 : 14,
|
||||
vertical: 12,
|
||||
horizontal: widget.collapsed ? 0 : 12,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.collapsed
|
||||
@ -209,7 +218,7 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
|
||||
children: [
|
||||
Icon(widget.section.icon, color: foreground, size: 20),
|
||||
if (!widget.collapsed) ...[
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
widget.section.label,
|
||||
style: Theme.of(
|
||||
@ -239,7 +248,9 @@ class SidebarFooter extends StatelessWidget {
|
||||
const SidebarFooter({
|
||||
super.key,
|
||||
required this.isCollapsed,
|
||||
required this.appLanguage,
|
||||
required this.themeMode,
|
||||
required this.onToggleLanguage,
|
||||
required this.onOpenThemeToggle,
|
||||
required this.onOpenSettings,
|
||||
required this.onToggleCollapsed,
|
||||
@ -248,7 +259,9 @@ class SidebarFooter extends StatelessWidget {
|
||||
});
|
||||
|
||||
final bool isCollapsed;
|
||||
final AppLanguage appLanguage;
|
||||
final ThemeMode themeMode;
|
||||
final VoidCallback onToggleLanguage;
|
||||
final VoidCallback onOpenThemeToggle;
|
||||
final VoidCallback onOpenSettings;
|
||||
final VoidCallback onToggleCollapsed;
|
||||
@ -257,8 +270,24 @@ class SidebarFooter extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final languageButton = Tooltip(
|
||||
message: appText('切换语言', 'Switch language'),
|
||||
child: isCollapsed
|
||||
? IconButton(
|
||||
onPressed: onToggleLanguage,
|
||||
icon: const Icon(Icons.translate_rounded),
|
||||
)
|
||||
: OutlinedButton.icon(
|
||||
onPressed: onToggleLanguage,
|
||||
icon: const Icon(Icons.translate_rounded, size: 18),
|
||||
label: Text(appLanguage.buttonLabel),
|
||||
),
|
||||
);
|
||||
|
||||
final themeButton = Tooltip(
|
||||
message: themeMode == ThemeMode.dark ? '切换浅色' : '切换暗色',
|
||||
message: themeMode == ThemeMode.dark
|
||||
? appText('切换浅色', 'Switch to light')
|
||||
: appText('切换深色', 'Switch to dark'),
|
||||
child: IconButton(
|
||||
onPressed: onOpenThemeToggle,
|
||||
icon: Icon(
|
||||
@ -270,7 +299,7 @@ class SidebarFooter extends StatelessWidget {
|
||||
);
|
||||
|
||||
final settingsButton = Tooltip(
|
||||
message: '打开设置',
|
||||
message: appText('打开设置', 'Open settings'),
|
||||
child: IconButton(
|
||||
onPressed: onOpenSettings,
|
||||
icon: const Icon(Icons.settings_rounded),
|
||||
@ -278,7 +307,9 @@ class SidebarFooter extends StatelessWidget {
|
||||
);
|
||||
|
||||
final collapseButton = Tooltip(
|
||||
message: isCollapsed ? '展开导航' : '折叠导航',
|
||||
message: isCollapsed
|
||||
? appText('展开导航', 'Expand sidebar')
|
||||
: appText('折叠导航', 'Collapse sidebar'),
|
||||
child: IconButton(
|
||||
onPressed: onToggleCollapsed,
|
||||
icon: Icon(
|
||||
@ -295,9 +326,11 @@ class SidebarFooter extends StatelessWidget {
|
||||
Column(
|
||||
children: [
|
||||
themeButton,
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 6),
|
||||
languageButton,
|
||||
const SizedBox(height: 6),
|
||||
settingsButton,
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 6),
|
||||
collapseButton,
|
||||
],
|
||||
)
|
||||
@ -306,30 +339,34 @@ class SidebarFooter extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [themeButton, settingsButton, collapseButton],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (!isCollapsed) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(width: double.infinity, child: languageButton),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Tooltip(
|
||||
message: isCollapsed ? 'Account' : '',
|
||||
message: isCollapsed ? appText('账号', 'Account') : '',
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: onOpenAccount,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isCollapsed ? 0 : 14,
|
||||
vertical: 12,
|
||||
horizontal: isCollapsed ? 0 : 12,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: accountSelected
|
||||
? context.palette.accentMuted
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: isCollapsed
|
||||
? const Icon(Icons.account_circle_rounded)
|
||||
: Row(
|
||||
children: [
|
||||
const CircleAvatar(radius: 18, child: Text('H')),
|
||||
const SizedBox(width: 12),
|
||||
const CircleAvatar(radius: 16, child: Text('H')),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -338,7 +375,7 @@ class SidebarFooter extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
Text(
|
||||
'Account',
|
||||
appText('账号', 'Account'),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
|
||||
@ -6,9 +6,13 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
)
|
||||
|
||||
|
||||
@ -6,12 +6,14 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import device_info_plus
|
||||
import file_selector_macos
|
||||
import flutter_secure_storage_macos
|
||||
import package_info_plus
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
PODS:
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_secure_storage_macos (6.1.3):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
@ -12,6 +14,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
@ -20,6 +23,8 @@ DEPENDENCIES:
|
||||
EXTERNAL SOURCES:
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
flutter_secure_storage_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||
FlutterMacOS:
|
||||
@ -31,6 +36,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
|
||||
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
|
||||
85
pubspec.lock
85
pubspec.lock
@ -49,6 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -113,6 +121,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_selector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_selector
|
||||
sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
file_selector_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_android
|
||||
sha256: "51e8fd0446de75e4b62c065b76db2210c704562d072339d333bd89c57a7f8a7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.2+4"
|
||||
file_selector_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_ios
|
||||
sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3+5"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.5"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
file_selector_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_web
|
||||
sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+2"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+5"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -126,6 +198,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -216,6 +293,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -30,6 +30,8 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
@ -37,6 +39,7 @@ dependencies:
|
||||
cryptography: ^2.6.1
|
||||
crypto: ^3.0.6
|
||||
device_info_plus: ^11.5.0
|
||||
file_selector: ^1.0.3
|
||||
flutter_secure_storage: ^9.2.4
|
||||
package_info_plus: ^8.3.1
|
||||
shared_preferences: ^2.5.3
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:xworkmate/app/app.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders XWorkmate shell', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const XWorkmateApp());
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(1600, 1000);
|
||||
addTearDown(() {
|
||||
tester.view.resetPhysicalSize();
|
||||
tester.view.resetDevicePixelRatio();
|
||||
});
|
||||
|
||||
expect(find.text('Assistant'), findsWidgets);
|
||||
expect(
|
||||
find.text('Connect a gateway to start chatting and running tasks.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
await tester.pumpWidget(const XWorkmateApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('助手'), findsWidgets);
|
||||
expect(find.text('连接 Gateway 后可开始对话和运行任务。'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@ -6,9 +6,12 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user