feat: 重构左侧边栏 - 添加 ClawHub 和 AI Gateway,将设置移至底部
This commit is contained in:
parent
19f49acdd6
commit
4d2cb683f2
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../features/account/account_page.dart';
|
||||
import '../features/ai_gateway/ai_gateway_page.dart';
|
||||
import '../features/assistant/assistant_page.dart';
|
||||
import '../features/claw_hub/claw_hub_page.dart';
|
||||
import '../features/mobile/ios_mobile_shell.dart';
|
||||
import '../features/modules/modules_page.dart';
|
||||
import '../features/secrets/secrets_page.dart';
|
||||
@ -327,10 +329,18 @@ class _AppShellState extends State<AppShell> {
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.clawHub => ClawHubPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.secrets => SecretsPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.aiGateway => AiGatewayPage(
|
||||
controller: widget.controller,
|
||||
onOpenDetail: onOpenDetail,
|
||||
),
|
||||
WorkspaceDestination.settings => SettingsPage(
|
||||
controller: widget.controller,
|
||||
),
|
||||
|
||||
393
lib/features/ai_gateway/ai_gateway_page.dart
Normal file
393
lib/features/ai_gateway/ai_gateway_page.dart
Normal file
@ -0,0 +1,393 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../widgets/metric_card.dart';
|
||||
import '../../widgets/section_header.dart';
|
||||
import '../../widgets/section_tabs.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../widgets/top_bar.dart';
|
||||
|
||||
class AiGatewayPage extends StatefulWidget {
|
||||
const AiGatewayPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
|
||||
@override
|
||||
State<AiGatewayPage> createState() => _AiGatewayPageState();
|
||||
}
|
||||
|
||||
class _AiGatewayPageState extends State<AiGatewayPage> {
|
||||
AiGatewayTab _tab = AiGatewayTab.models;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = widget.controller;
|
||||
final palette = context.palette;
|
||||
|
||||
final metrics = [
|
||||
MetricSummary(
|
||||
label: appText('网关状态', 'Gateway'),
|
||||
value: controller.connection.status.label,
|
||||
caption: controller.connection.remoteAddress ?? appText('未连接', 'Disconnected'),
|
||||
icon: Icons.wifi_tethering_rounded,
|
||||
status: _connectionStatus(controller.connection.status),
|
||||
),
|
||||
MetricSummary(
|
||||
label: appText('活跃模型', 'Active Models'),
|
||||
value: '${controller.models.length}',
|
||||
caption: controller.models.isNotEmpty
|
||||
? controller.models.first.name
|
||||
: appText('无', 'None'),
|
||||
icon: Icons.psychology_rounded,
|
||||
),
|
||||
MetricSummary(
|
||||
label: appText('代理', 'Agents'),
|
||||
value: '${controller.agents.length}',
|
||||
caption: controller.activeAgentName,
|
||||
icon: Icons.hub_rounded,
|
||||
),
|
||||
];
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(32, 32, 32, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
title: 'AI Gateway',
|
||||
subtitle: appText(
|
||||
'AI 代理与模型网关配置管理中心。',
|
||||
'AI proxy and model gateway configuration center.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: metrics.map((m) => MetricCard(metric: m)).toList(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SectionTabs<AiGatewayTab>(
|
||||
tabs: AiGatewayTab.values,
|
||||
selected: _tab,
|
||||
onSelect: (t) => setState(() => _tab = t),
|
||||
labelFor: (t) => t.label,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTabContent(context, _tab, controller),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabContent(BuildContext context, AiGatewayTab tab, AppController controller) {
|
||||
final palette = context.palette;
|
||||
|
||||
switch (tab) {
|
||||
case AiGatewayTab.models:
|
||||
return SurfaceCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.psychology_rounded, color: palette.accent, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
appText('模型列表', 'Model List'),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: palette.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
label: Text(appText('添加模型', 'Add Model')),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (controller.models.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
appText('暂无配置的模型', 'No models configured'),
|
||||
style: TextStyle(color: palette.textSecondary),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...controller.models.map((model) => _ModelCard(
|
||||
model: model,
|
||||
onTap: () {},
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
case AiGatewayTab.agents:
|
||||
return SurfaceCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.hub_rounded, color: palette.accent, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
appText('代理列表', 'Agent List'),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: palette.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
label: Text(appText('添加代理', 'Add Agent')),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (controller.agents.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Text(
|
||||
appText('暂无配置的代理', 'No agents configured'),
|
||||
style: TextStyle(color: palette.textSecondary),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...controller.agents.map((agent) => _AgentCard(
|
||||
agent: agent,
|
||||
onTap: () {},
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
case AiGatewayTab.endpoints:
|
||||
return SurfaceCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.endpoint_rounded, color: palette.accent, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
appText('端点配置', 'Endpoint Configuration'),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: palette.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EndpointCard(
|
||||
name: 'OpenAI',
|
||||
endpoint: 'https://api.openai.com/v1',
|
||||
status: 'Connected',
|
||||
onTap: () {},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_EndpointCard(
|
||||
name: 'Azure OpenAI',
|
||||
endpoint: 'https://*.openai.azure.com',
|
||||
status: 'Disconnected',
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StatusInfo? _connectionStatus(RuntimeConnectionStatus status) {
|
||||
return switch (status) {
|
||||
RuntimeConnectionStatus.connected => const StatusInfo('Connected', StatusTone.success),
|
||||
RuntimeConnectionStatus.connecting => const StatusInfo('Connecting', StatusTone.accent),
|
||||
RuntimeConnectionStatus.disconnected => const StatusInfo('Disconnected', StatusTone.neutral),
|
||||
RuntimeConnectionStatus.error => const StatusInfo('Error', StatusTone.danger),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum AiGatewayTab { models, agents, endpoints }
|
||||
|
||||
extension AiGatewayTabCopy on AiGatewayTab {
|
||||
String get label => switch (this) {
|
||||
AiGatewayTab.models => appText('模型', 'Models'),
|
||||
AiGatewayTab.agents => appText('代理', 'Agents'),
|
||||
AiGatewayTab.endpoints => appText('端点', 'Endpoints'),
|
||||
};
|
||||
}
|
||||
|
||||
class _ModelCard extends StatelessWidget {
|
||||
const _ModelCard({required this.model, required this.onTap});
|
||||
|
||||
final dynamic model;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
color: palette.surfaceSecondary,
|
||||
elevation: 0,
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
leading: Icon(Icons.psychology_rounded, color: palette.accent),
|
||||
title: Text(model.name ?? 'Unknown', style: TextStyle(color: palette.textPrimary)),
|
||||
subtitle: Text(
|
||||
model.provider ?? 'Unknown provider',
|
||||
style: TextStyle(color: palette.textSecondary),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Active',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.green,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.chevron_right, color: palette.textMuted),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AgentCard extends StatelessWidget {
|
||||
const _AgentCard({required this.agent, required this.onTap});
|
||||
|
||||
final dynamic agent;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
color: palette.surfaceSecondary,
|
||||
elevation: 0,
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
leading: Icon(Icons.hub_rounded, color: palette.accent),
|
||||
title: Text(agent.name ?? 'Unknown', style: TextStyle(color: palette.textPrimary)),
|
||||
subtitle: Text(
|
||||
agent.capabilities?.join(', ') ?? 'No capabilities',
|
||||
style: TextStyle(color: palette.textSecondary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.chevron_right, color: palette.textMuted),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EndpointCard extends StatelessWidget {
|
||||
const _EndpointCard({
|
||||
required this.name,
|
||||
required this.endpoint,
|
||||
required this.status,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String endpoint;
|
||||
final String status;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
final isConnected = status == 'Connected';
|
||||
|
||||
return Card(
|
||||
color: palette.surfaceSecondary,
|
||||
elevation: 0,
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
leading: Icon(
|
||||
Icons.endpoint_rounded,
|
||||
color: isConnected ? palette.accent : palette.textMuted,
|
||||
),
|
||||
title: Text(name, style: TextStyle(color: palette.textPrimary)),
|
||||
subtitle: Text(
|
||||
endpoint,
|
||||
style: TextStyle(color: palette.textSecondary, fontFamily: 'monospace'),
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isConnected
|
||||
? Colors.green.withOpacity(0.2)
|
||||
: Colors.grey.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isConnected ? Colors.green : palette.textMuted,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
461
lib/features/claw_hub/claw_hub_page.dart
Normal file
461
lib/features/claw_hub/claw_hub_page.dart
Normal file
@ -0,0 +1,461 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../widgets/section_header.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../widgets/top_bar.dart';
|
||||
|
||||
class ClawHubPage extends StatefulWidget {
|
||||
const ClawHubPage({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onOpenDetail,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||
|
||||
@override
|
||||
State<ClawHubPage> createState() => _ClawHubPageState();
|
||||
}
|
||||
|
||||
class _ClawHubPageState extends State<ClawHubPage> {
|
||||
final _searchController = TextEditingController();
|
||||
final _commandController = TextEditingController();
|
||||
final _scrollController = ScrollController();
|
||||
final List<ClawHubLogEntry> _logs = [];
|
||||
bool _isExecuting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_commandController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _addLog(String message, {ClawHubLogType type = ClawHubLogType.info}) {
|
||||
setState(() {
|
||||
_logs.add(ClawHubLogEntry(
|
||||
timestamp: DateTime.now(),
|
||||
message: message,
|
||||
type: type,
|
||||
));
|
||||
});
|
||||
// Auto-scroll to bottom
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _executeCommand(String input) {
|
||||
if (input.trim().isEmpty) return;
|
||||
|
||||
_addLog('\$ clawhub \$input', type: ClawHubLogType.command);
|
||||
_commandController.clear();
|
||||
|
||||
final parts = input.trim().split(RegExp(r'\s+'));
|
||||
final command = parts.isNotEmpty ? parts[0] : '';
|
||||
final args = parts.length > 1 ? parts.sublist(1) : <String>[];
|
||||
|
||||
switch (command) {
|
||||
case 'search':
|
||||
_handleSearch(args);
|
||||
break;
|
||||
case 'install':
|
||||
_handleInstall(args);
|
||||
break;
|
||||
case 'update':
|
||||
_handleUpdate(args);
|
||||
break;
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
_showHelp();
|
||||
break;
|
||||
default:
|
||||
_addLog(
|
||||
'Unknown command: \$command. Type "clawhub help" for available commands.',
|
||||
type: ClawHubLogType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSearch(List<String> args) {
|
||||
final query = args.join(' ');
|
||||
if (query.isEmpty) {
|
||||
_addLog('Usage: clawhub search "<query>"', type: ClawHubLogType.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isExecuting = true);
|
||||
_addLog('Searching for "\$query"...');
|
||||
|
||||
// Simulate search results
|
||||
Future.delayed(const Duration(milliseconds: 800), () {
|
||||
setState(() => _isExecuting = false);
|
||||
_addLog('');
|
||||
_addLog('Found 3 packages:', type: ClawHubLogType.success);
|
||||
_addLog(' ├─ skill-analyzer v1.2.0 Code analysis skill');
|
||||
_addLog(' ├─ feishu-connector v2.1.3 Feishu integration');
|
||||
_addLog(' └─ azure-deploy v3.0.1 Azure deployment helper');
|
||||
_addLog('');
|
||||
_addLog('Use "clawhub install <slug>" to install a package.');
|
||||
});
|
||||
}
|
||||
|
||||
void _handleInstall(List<String> args) {
|
||||
if (args.isEmpty) {
|
||||
_addLog('Usage: clawhub install <slug>', type: ClawHubLogType.warning);
|
||||
return;
|
||||
}
|
||||
|
||||
final slug = args[0];
|
||||
setState(() => _isExecuting = true);
|
||||
_addLog('Installing \$slug...');
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 1200), () {
|
||||
setState(() => _isExecuting = false);
|
||||
_addLog('✓ Successfully installed \$slug', type: ClawHubLogType.success);
|
||||
_addLog(' Location: ~/.clawhub/skills/\$slug');
|
||||
_addLog(' Run "clawhub update" to check for updates.');
|
||||
});
|
||||
}
|
||||
|
||||
void _handleUpdate(List<String> args) {
|
||||
final isAll = args.contains('--all') || args.contains('-a');
|
||||
final slug = isAll ? null : (args.isNotEmpty ? args[0] : null);
|
||||
|
||||
setState(() => _isExecuting = true);
|
||||
|
||||
if (isAll) {
|
||||
_addLog('Checking for updates...');
|
||||
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
setState(() => _isExecuting = false);
|
||||
_addLog('✓ All packages are up to date', type: ClawHubLogType.success);
|
||||
});
|
||||
} else if (slug != null) {
|
||||
_addLog('Updating \$slug...');
|
||||
Future.delayed(const Duration(milliseconds: 800), () {
|
||||
setState(() => _isExecuting = false);
|
||||
_addLog('✓ \$slug updated to latest version', type: ClawHubLogType.success);
|
||||
});
|
||||
} else {
|
||||
_addLog('Usage: clawhub update <slug> or clawhub update --all',
|
||||
type: ClawHubLogType.warning);
|
||||
setState(() => _isExecuting = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showHelp() {
|
||||
_addLog('');
|
||||
_addLog('ClawHub Package Manager', type: ClawHubLogType.success);
|
||||
_addLog('Usage: clawhub <command> [options]');
|
||||
_addLog('');
|
||||
_addLog('Commands:');
|
||||
_addLog(' search "<query>" Search for packages');
|
||||
_addLog(' install <slug> Install a package');
|
||||
_addLog(' update <slug> Update a specific package');
|
||||
_addLog(' update --all Update all packages');
|
||||
_addLog(' help Show this help message');
|
||||
_addLog('');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: widget.controller,
|
||||
builder: (context, _) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(32, 32, 32, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
title: 'ClawHub',
|
||||
subtitle: appText(
|
||||
'NPM 风格的包管理中心,支持搜索、安装和更新 Skills。',
|
||||
'NPM-style package manager for skills.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SectionHeader(
|
||||
title: appText('终端', 'Terminal'),
|
||||
icon: Icons.terminal_rounded,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SurfaceCard(
|
||||
child: Container(
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfaceSecondary.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Terminal header
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfaceSecondary,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.terminal_rounded,
|
||||
size: 16,
|
||||
color: palette.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'clawhub',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: palette.textSecondary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_isExecuting)
|
||||
SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: palette.accent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Terminal output
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: _logs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final log = _logs[index];
|
||||
return _LogLine(entry: log, palette: palette);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// Command input
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfaceSecondary,
|
||||
border: Border(
|
||||
top: BorderSide(color: palette.strokeSoft),
|
||||
),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
bottom: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'\$',
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: palette.accent,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _commandController,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
color: palette.textPrimary,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: appText(
|
||||
'输入命令 (search, install, update)',
|
||||
'Type command (search, install, update)',
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 14,
|
||||
color: palette.textMuted,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
onSubmitted: _executeCommand,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.send_rounded,
|
||||
size: 18,
|
||||
color: palette.accent,
|
||||
),
|
||||
onPressed: () =>
|
||||
_executeCommand(_commandController.text),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SectionHeader(
|
||||
title: appText('快速操作', 'Quick Actions'),
|
||||
icon: Icons.bolt_rounded,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
_QuickActionButton(
|
||||
icon: Icons.search_rounded,
|
||||
label: appText('搜索技能', 'Search Skills'),
|
||||
onTap: () => _executeCommand('search analytics'),
|
||||
),
|
||||
_QuickActionButton(
|
||||
icon: Icons.download_rounded,
|
||||
label: appText('安装技能', 'Install Skill'),
|
||||
onTap: () => _executeCommand('install example-skill'),
|
||||
),
|
||||
_QuickActionButton(
|
||||
icon: Icons.update_rounded,
|
||||
label: appText('更新全部', 'Update All'),
|
||||
onTap: () => _executeCommand('update --all'),
|
||||
),
|
||||
_QuickActionButton(
|
||||
icon: Icons.help_outline_rounded,
|
||||
label: appText('查看帮助', 'View Help'),
|
||||
onTap: () => _executeCommand('help'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ClawHubLogType { info, command, success, warning, error }
|
||||
|
||||
class ClawHubLogEntry {
|
||||
final DateTime timestamp;
|
||||
final String message;
|
||||
final ClawHubLogType type;
|
||||
|
||||
ClawHubLogEntry({
|
||||
required this.timestamp,
|
||||
required this.message,
|
||||
required this.type,
|
||||
});
|
||||
}
|
||||
|
||||
class _LogLine extends StatelessWidget {
|
||||
const _LogLine({required this.entry, required this.palette});
|
||||
|
||||
final ClawHubLogEntry entry;
|
||||
final AppPalette palette;
|
||||
|
||||
Color get _color {
|
||||
switch (entry.type) {
|
||||
case ClawHubLogType.command:
|
||||
return palette.accent;
|
||||
case ClawHubLogType.success:
|
||||
return Colors.green;
|
||||
case ClawHubLogType.warning:
|
||||
return Colors.orange;
|
||||
case ClawHubLogType.error:
|
||||
return Colors.red;
|
||||
case ClawHubLogType.info:
|
||||
return palette.textPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
entry.message,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13,
|
||||
color: _color,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickActionButton extends StatelessWidget {
|
||||
const _QuickActionButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
|
||||
return Material(
|
||||
color: palette.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 18, color: palette.accent),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: palette.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -44,8 +44,12 @@ extension WorkspaceDestinationCopy on WorkspaceDestination {
|
||||
'Capability center for gateway, nodes, agents, skills, and connectors.',
|
||||
),
|
||||
WorkspaceDestination.secrets => appText(
|
||||
'Vault、Provider 凭证与审计信息的轻量管理面。',
|
||||
'Lightweight management for vault, provider credentials, and audit data.',
|
||||
'Vault 密码保险箱,安全存储密钥、凭证与审计信息。',
|
||||
'Vault password safe for secure storage of keys, credentials and audit data.',
|
||||
),
|
||||
WorkspaceDestination.aiGateway => appText(
|
||||
'AI Gateway 代理与模型网关配置管理。',
|
||||
'AI Gateway proxy and model gateway configuration.',
|
||||
),
|
||||
WorkspaceDestination.settings => appText(
|
||||
'全局配置中心,只负责系统设置与诊断,不承担业务模块入口。',
|
||||
|
||||
@ -41,8 +41,9 @@ class SidebarNavigation extends StatelessWidget {
|
||||
WorkspaceDestination.assistant,
|
||||
WorkspaceDestination.tasks,
|
||||
WorkspaceDestination.modules,
|
||||
WorkspaceDestination.clawHub,
|
||||
WorkspaceDestination.secrets,
|
||||
WorkspaceDestination.settings,
|
||||
WorkspaceDestination.aiGateway,
|
||||
];
|
||||
|
||||
@override
|
||||
@ -253,7 +254,9 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
|
||||
WorkspaceDestination.assistant => Icons.auto_awesome_rounded,
|
||||
WorkspaceDestination.tasks => Icons.task_alt_rounded,
|
||||
WorkspaceDestination.modules => Icons.extension_rounded,
|
||||
WorkspaceDestination.secrets => Icons.key_rounded,
|
||||
WorkspaceDestination.clawHub => Icons.hub_rounded,
|
||||
WorkspaceDestination.secrets => Icons.security_rounded,
|
||||
WorkspaceDestination.aiGateway => Icons.smart_toy_rounded,
|
||||
WorkspaceDestination.settings => Icons.tune_rounded,
|
||||
WorkspaceDestination.account => Icons.account_circle_rounded,
|
||||
};
|
||||
@ -264,7 +267,9 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
|
||||
WorkspaceDestination.assistant => appText('助手', 'Assistant'),
|
||||
WorkspaceDestination.tasks => appText('任务', 'Tasks'),
|
||||
WorkspaceDestination.modules => appText('模块', 'Modules'),
|
||||
WorkspaceDestination.secrets => appText('密钥', 'Secrets'),
|
||||
WorkspaceDestination.clawHub => 'ClawHub',
|
||||
WorkspaceDestination.secrets => appText('密钥 / Vault', 'Secrets / Vault'),
|
||||
WorkspaceDestination.aiGateway => 'AI Gateway',
|
||||
WorkspaceDestination.settings => appText('设置', 'Settings'),
|
||||
WorkspaceDestination.account => appText('账户', 'Account'),
|
||||
};
|
||||
@ -334,6 +339,12 @@ class SidebarFooter extends StatelessWidget {
|
||||
onPressed: onCycleSidebarState,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
_SidebarActionButton(
|
||||
icon: Icons.tune_rounded,
|
||||
tooltip: appText('设置', 'Settings'),
|
||||
onPressed: onOpenSettings,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
_SidebarAccountTile(
|
||||
selected: accountSelected,
|
||||
onTap: onOpenAccount,
|
||||
@ -375,6 +386,12 @@ class SidebarFooter extends StatelessWidget {
|
||||
tooltip: _sidebarStateLabel(sidebarState),
|
||||
onPressed: onCycleSidebarState,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
_SidebarActionButton(
|
||||
icon: Icons.tune_rounded,
|
||||
tooltip: appText('设置', 'Settings'),
|
||||
onPressed: onOpenSettings,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
@ -390,8 +407,9 @@ class SidebarFooter extends StatelessWidget {
|
||||
|
||||
IconData _sidebarStateIcon(AppSidebarState state) {
|
||||
return switch (state) {
|
||||
AppSidebarState.expanded => Icons.sidebar_rounded,
|
||||
AppSidebarState.expanded => Icons.view_sidebar_rounded,
|
||||
AppSidebarState.collapsed => Icons.menu_rounded,
|
||||
AppSidebarState.hidden => Icons.view_sidebar_rounded,
|
||||
};
|
||||
}
|
||||
|
||||
@ -399,6 +417,7 @@ class SidebarFooter extends StatelessWidget {
|
||||
return switch (state) {
|
||||
AppSidebarState.expanded => appText('收起侧边栏', 'Collapse sidebar'),
|
||||
AppSidebarState.collapsed => appText('展开侧边栏', 'Expand sidebar'),
|
||||
AppSidebarState.hidden => appText('展开侧边栏', 'Expand sidebar'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ name: xworkmate
|
||||
description: "XWorkmate desktop-first AI workspace shell."
|
||||
publish_to: 'none'
|
||||
|
||||
version: latest
|
||||
version: 0.1.0+1
|
||||
build-date: 2026-03-12
|
||||
build-id: acc3a06
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user