xworkspace-console/lib/main.dart
2026-06-06 18:38:45 +08:00

1153 lines
31 KiB
Dart

import 'package:flutter/material.dart';
void main() {
runApp(const XWorkspaceApp());
}
enum ConsolePage { workspace, openclaw, litellm, vault, terminal }
class XWorkspaceApp extends StatelessWidget {
const XWorkspaceApp({super.key});
@override
Widget build(BuildContext context) {
final scheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF2F6FED),
brightness: Brightness.light,
);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'XWorkspace Console',
theme: ThemeData(
colorScheme: scheme,
useMaterial3: true,
scaffoldBackgroundColor: const Color(0xFFEAE3D8),
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w700,
letterSpacing: -0.6,
),
headlineMedium: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
),
titleLarge: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
titleMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
bodyMedium: TextStyle(fontSize: 13, height: 1.35),
),
),
home: const ConsoleShell(),
);
}
}
class ConsoleShell extends StatefulWidget {
const ConsoleShell({super.key});
@override
State<ConsoleShell> createState() => _ConsoleShellState();
}
class _ConsoleShellState extends State<ConsoleShell> {
ConsolePage _page = ConsolePage.workspace;
static const _titles = <ConsolePage, String>{
ConsolePage.workspace: 'Workspace',
ConsolePage.openclaw: 'OpenClaw',
ConsolePage.litellm: 'LiteLLM',
ConsolePage.vault: 'Vault',
ConsolePage.terminal: 'Terminal',
};
static const _subtitles = <ConsolePage, String>{
ConsolePage.workspace: 'Default home view for the workspace control plane.',
ConsolePage.openclaw:
'Gateway and bridge status for the workspace edge layer.',
ConsolePage.litellm:
'Model router dashboard with latency, throughput, and cost.',
ConsolePage.vault: 'Secrets, policy, and access posture for the workspace.',
ConsolePage.terminal:
'Dedicated shell surface for ttyd or future terminal embedding.',
};
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isCompact = constraints.maxWidth < 980;
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(18),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(isCompact ? 0 : 30),
border: Border.all(color: const Color(0x1A4A3C2A)),
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFF9F5EE),
Color(0xFFF0E9DE),
Color(0xFFE3DACE),
],
),
boxShadow: const [
BoxShadow(
color: Color(0x1A3F2E16),
blurRadius: 28,
offset: Offset(0, 18),
),
],
),
clipBehavior: Clip.antiAlias,
child: isCompact
? _CompactShell(
page: _page,
onPageSelected: _setPage,
title: _titles[_page]!,
subtitle: _subtitles[_page]!,
body: _buildPage(_page),
)
: Row(
children: [
_Sidebar(page: _page, onPageSelected: _setPage),
Expanded(
child: Column(
children: [
_TopBar(
title: _titles[_page]!,
subtitle: _subtitles[_page]!,
),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
child: _buildPage(
_page,
key: ValueKey(_page),
),
),
),
],
),
),
],
),
),
),
);
},
);
}
void _setPage(ConsolePage page) {
if (page == _page) return;
setState(() => _page = page);
}
Widget _buildPage(ConsolePage page, {Key? key}) {
return KeyedSubtree(
key: key,
child: switch (page) {
ConsolePage.workspace => const WorkspacePage(),
ConsolePage.openclaw => const OpenClawPage(),
ConsolePage.litellm => const ModelPage(),
ConsolePage.vault => const VaultPage(),
ConsolePage.terminal => const TerminalPage(),
},
);
}
}
class _CompactShell extends StatelessWidget {
const _CompactShell({
required this.page,
required this.onPageSelected,
required this.title,
required this.subtitle,
required this.body,
});
final ConsolePage page;
final ValueChanged<ConsolePage> onPageSelected;
final String title;
final String subtitle;
final Widget body;
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 12),
child: _TopBar(title: title, subtitle: subtitle),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: ConsolePage.values
.map(
(entry) => Padding(
padding: const EdgeInsets.only(right: 10),
child: _NavPill(
label: _ShellLabels.label(entry),
selected: page == entry,
onTap: () => onPageSelected(entry),
),
),
)
.toList(),
),
),
),
const SizedBox(height: 16),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(18, 0, 18, 18),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
child: body,
),
),
),
],
);
}
}
class _Sidebar extends StatelessWidget {
const _Sidebar({required this.page, required this.onPageSelected});
final ConsolePage page;
final ValueChanged<ConsolePage> onPageSelected;
@override
Widget build(BuildContext context) {
return Container(
width: 220,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF25201B), Color(0xFF3A3128)],
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _Brand(),
const SizedBox(height: 18),
for (final item in ConsolePage.values) ...[
_NavItem(
label: _ShellLabels.label(item),
selected: page == item,
onTap: () => onPageSelected(item),
),
const SizedBox(height: 8),
],
const Spacer(),
const Text(
'MVP HTML shell now,\nFlutter Web later.',
style: TextStyle(
color: Color(0x99F7F1E9),
fontSize: 12,
height: 1.45,
),
),
],
),
),
);
}
}
class _TopBar extends StatelessWidget {
const _TopBar({required this.title, required this.subtitle});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(28, 24, 28, 18),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Color(0x1F4A3C2A))),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.headlineLarge),
const SizedBox(height: 6),
Text(
subtitle,
style: TextStyle(color: Colors.brown.shade700, fontSize: 13),
),
],
),
),
const _StatusChip(),
],
),
);
}
}
class _StatusChip extends StatelessWidget {
const _StatusChip();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.74),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0x194A3C2A)),
boxShadow: const [
BoxShadow(
color: Color(0x14000000),
blurRadius: 20,
offset: Offset(0, 8),
),
],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
_Dot(color: Color(0xFF4FD17B)),
SizedBox(width: 8),
Text(
'System healthy',
style: TextStyle(fontSize: 13, color: Color(0xFF6B6258)),
),
],
),
);
}
}
class _Dot extends StatelessWidget {
const _Dot({required this.color});
final Color color;
@override
Widget build(BuildContext context) {
return Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.18),
blurRadius: 0,
spreadRadius: 5,
),
],
),
);
}
}
class _Brand extends StatelessWidget {
const _Brand();
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'XWorkspace',
style: TextStyle(
color: Color(0xFFF7F1E9),
fontSize: 18,
fontWeight: FontWeight.w700,
letterSpacing: 2,
),
),
SizedBox(height: 6),
Text(
'AI Workspace Control Plane',
style: TextStyle(color: Color(0x99F7F1E9), fontSize: 12),
),
],
);
}
}
class _NavItem extends StatelessWidget {
const _NavItem({
required this.label,
required this.selected,
required this.onTap,
});
final String label;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: selected ? const Color(0x1FFFFFFF) : Colors.transparent,
borderRadius: BorderRadius.circular(14),
border: selected ? Border.all(color: const Color(0x14FFFFFF)) : null,
),
child: Row(
children: [
Expanded(
child: Text(
label,
style: TextStyle(
color: selected ? Colors.white : const Color(0xD0F7F1E9),
fontSize: 14,
),
),
),
_Dot(
color: selected
? const Color(0xFF78D27F)
: const Color(0x33FFFFFF),
),
],
),
),
);
}
}
class _NavPill extends StatelessWidget {
const _NavPill({
required this.label,
required this.selected,
required this.onTap,
});
final String label;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onTap,
style: TextButton.styleFrom(
backgroundColor: selected
? const Color(0xFFE4EAF8)
: Colors.white.withValues(alpha: 0.72),
foregroundColor: selected
? const Color(0xFF2459C9)
: const Color(0xFF4B4238),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
child: Text(label),
);
}
}
class WorkspacePage extends StatelessWidget {
const WorkspacePage({super.key});
@override
Widget build(BuildContext context) {
return ListView(
key: const PageStorageKey('workspace'),
padding: const EdgeInsets.fromLTRB(28, 18, 28, 28),
children: const [
_OverviewGrid(),
SizedBox(height: 18),
_TerminalCard(
title: 'Embedded Terminal',
subtitle: 'ttyd placeholder for the live shell experience',
),
],
);
}
}
class OpenClawPage extends StatelessWidget {
const OpenClawPage({super.key});
@override
Widget build(BuildContext context) {
return ListView(
key: const PageStorageKey('openclaw'),
padding: const EdgeInsets.fromLTRB(28, 18, 28, 28),
children: const [
_TwoPanel(
left: _MetricCardGrid(
title: 'OpenClaw Gateway',
subtitle: 'Ingress, authorization, and routing status',
cards: [
_MetricData('Requests', '1.8M', '24h total'),
_MetricData('Errors', '0.2%', 'rolling avg'),
_MetricData('P95', '142ms', 'response time'),
_MetricData('Auth', 'OK', 'vault-linked'),
],
),
right: _ServicePanel(
title: 'Bridge',
subtitle: 'Active sessions and shell bridging',
rows: [
_ServiceRow('ttyd channel', 'Connected sessions', '12 live'),
_ServiceRow('Workspace sync', 'State propagation', 'Healthy'),
_ServiceRow('Event queue', 'Backlog', '0 pending'),
],
),
),
],
);
}
}
class ModelPage extends StatelessWidget {
const ModelPage({super.key});
@override
Widget build(BuildContext context) {
return ListView(
key: const PageStorageKey('litellm'),
padding: const EdgeInsets.fromLTRB(28, 18, 28, 28),
children: const [_ModelsCard()],
);
}
}
class VaultPage extends StatelessWidget {
const VaultPage({super.key});
@override
Widget build(BuildContext context) {
return ListView(
key: const PageStorageKey('vault'),
padding: const EdgeInsets.fromLTRB(28, 18, 28, 28),
children: const [
_TwoPanel(
left: _MetricCardGrid(
title: 'Vault',
subtitle: 'Secrets, token rotation, and access policy',
cards: [
_MetricData('Secrets', '42', 'active items'),
_MetricData('Rotations', '7d', 'next cycle'),
_MetricData('Scopes', '5', 'policy groups'),
_MetricData('Alerts', '0', 'open issues'),
],
),
right: _TerminalCard(
title: 'Terminal',
subtitle: 'Minimal shell access entry point',
compactHeight: 240,
headerOnly: true,
),
),
],
);
}
}
class TerminalPage extends StatelessWidget {
const TerminalPage({super.key});
@override
Widget build(BuildContext context) {
return ListView(
key: const PageStorageKey('terminal'),
padding: const EdgeInsets.fromLTRB(28, 18, 28, 28),
children: const [
_TerminalCard(
title: 'Embedded Terminal',
subtitle: 'Dedicated shell screen for ttyd integration',
compactHeight: 520,
),
],
);
}
}
class _OverviewGrid extends StatelessWidget {
const _OverviewGrid();
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 980;
return Wrap(
spacing: 18,
runSpacing: 18,
children: [
SizedBox(
width: narrow
? constraints.maxWidth
: (constraints.maxWidth - 18) * 0.58,
child: const _ServicePanel(
title: 'Services',
subtitle: 'OpenClaw Gateway, Bridge, LiteLLM, Vault',
rows: [
_ServiceRow(
'OpenClaw Gateway',
'Policy + ingress layer',
'Running',
),
_ServiceRow('Bridge', 'Workspace session bridge', 'Running'),
_ServiceRow('LiteLLM', 'Model router and proxy', 'Running'),
_ServiceRow(
'Vault',
'Secrets and credential store',
'Running',
),
],
),
),
SizedBox(
width: narrow
? constraints.maxWidth
: (constraints.maxWidth - 18) * 0.40,
child: const _MetricCardGrid(
title: 'Runtime',
subtitle: 'Capacity and health snapshot',
cards: [
_MetricData('CPU', '18%', 'steady load'),
_MetricData('Memory', '4.1G', '/ 8G used'),
_MetricData('Disk', '23G', '/ 80G used'),
_MetricData('Uptime', '12d', '2h 41m'),
],
),
),
],
);
},
);
}
}
class _TwoPanel extends StatelessWidget {
const _TwoPanel({required this.left, required this.right});
final Widget left;
final Widget right;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 980;
return Wrap(
spacing: 18,
runSpacing: 18,
children: [
SizedBox(
width: narrow
? constraints.maxWidth
: (constraints.maxWidth - 18) * 0.58,
child: left,
),
SizedBox(
width: narrow
? constraints.maxWidth
: (constraints.maxWidth - 18) * 0.40,
child: right,
),
],
);
},
);
}
}
class _CardShell extends StatelessWidget {
const _CardShell({
required this.title,
required this.subtitle,
required this.child,
});
final String title;
final String subtitle;
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.70),
borderRadius: BorderRadius.circular(28),
border: Border.all(color: const Color(0x194A3C2A)),
boxShadow: const [
BoxShadow(
color: Color(0x144F371B),
blurRadius: 36,
offset: Offset(0, 16),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(22, 20, 22, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(color: Colors.brown.shade700, fontSize: 12),
),
],
),
),
const Divider(height: 1),
Padding(padding: const EdgeInsets.all(20), child: child),
],
),
);
}
}
class _ServicePanel extends StatelessWidget {
const _ServicePanel({
required this.title,
required this.subtitle,
required this.rows,
});
final String title;
final String subtitle;
final List<_ServiceRow> rows;
@override
Widget build(BuildContext context) {
return _CardShell(
title: title,
subtitle: subtitle,
child: Column(
children: [
for (final row in rows) ...[
_ServiceTile(row: row),
if (row != rows.last) const SizedBox(height: 12),
],
],
),
);
}
}
class _MetricCardGrid extends StatelessWidget {
const _MetricCardGrid({
required this.title,
required this.subtitle,
required this.cards,
});
final String title;
final String subtitle;
final List<_MetricData> cards;
@override
Widget build(BuildContext context) {
return _CardShell(
title: title,
subtitle: subtitle,
child: LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 420;
return Wrap(
spacing: 14,
runSpacing: 14,
children: [
for (final item in cards)
SizedBox(
width: narrow
? constraints.maxWidth
: (constraints.maxWidth - 14) / 2,
child: _MetricTile(data: item),
),
],
);
},
),
);
}
}
class _ModelsCard extends StatelessWidget {
const _ModelsCard();
@override
Widget build(BuildContext context) {
return _CardShell(
title: 'Models',
subtitle: 'Future LiteLLM view with latency, RPM, and cost',
child: Column(
children: [
TextField(
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
hintText: 'Search model, provider, status...',
filled: true,
fillColor: Colors.white.withValues(alpha: 0.78),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0x194A3C2A)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0x194A3C2A)),
),
),
),
const SizedBox(height: 18),
Table(
columnWidths: const {
0: FlexColumnWidth(1.2),
1: FlexColumnWidth(1),
2: FlexColumnWidth(0.8),
3: FlexColumnWidth(0.7),
4: FlexColumnWidth(0.8),
},
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
_headerRow(),
_modelRow('GPT-5', 'Ready', '128 ms', '620', '\$0.00 / req'),
_modelRow('Claude', 'Ready', '145 ms', '540', '\$0.00 / req'),
_modelRow('Gemini', 'Ready', '132 ms', '480', '\$0.00 / req'),
_modelRow('DeepSeek', 'Ready', '160 ms', '510', '\$0.00 / req'),
],
),
],
),
);
}
}
class _TerminalCard extends StatelessWidget {
const _TerminalCard({
required this.title,
required this.subtitle,
this.compactHeight = 220,
this.headerOnly = false,
});
final String title;
final String subtitle;
final double compactHeight;
final bool headerOnly;
@override
Widget build(BuildContext context) {
return _CardShell(
title: title,
subtitle: subtitle,
child: Container(
height: compactHeight,
decoration: BoxDecoration(
color: const Color(0xFF141619),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: const Color(0x1AFFFFFF)),
),
padding: const EdgeInsets.all(18),
child: DefaultTextStyle(
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13,
color: Color(0xFFCCF6CC),
height: 1.5,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text.rich(
TextSpan(
children: [
TextSpan(
text: 'ubuntu@workspace:~\$',
style: TextStyle(color: Color(0xFF7DD9A2)),
),
TextSpan(
text: ' openclaw status',
style: TextStyle(color: Color(0xFFB3CBB8)),
),
],
),
),
const SizedBox(height: 8),
const Text(
'Gateway Running',
style: TextStyle(color: Color(0xFFB3CBB8)),
),
const Text(
'Bridge Running',
style: TextStyle(color: Color(0xFFB3CBB8)),
),
const Text(
'LiteLLM Running',
style: TextStyle(color: Color(0xFFB3CBB8)),
),
const Text(
'Vault Running',
style: TextStyle(color: Color(0xFFB3CBB8)),
),
if (!headerOnly) const Spacer(),
const Text.rich(
TextSpan(
children: [
TextSpan(
text: 'ubuntu@workspace:~\$',
style: TextStyle(color: Color(0xFF7DD9A2)),
),
TextSpan(
text: ' _',
style: TextStyle(color: Color(0xFFB3CBB8)),
),
],
),
),
],
),
),
),
);
}
}
class _MetricTile extends StatelessWidget {
const _MetricTile({required this.data});
final _MetricData data;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.72),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0x144A3C2A)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.label,
style: const TextStyle(fontSize: 12, color: Color(0xFF6D645B)),
),
const SizedBox(height: 8),
Text(
data.value,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
),
),
const SizedBox(height: 4),
Text(
data.subtext,
style: const TextStyle(fontSize: 12, color: Color(0xFF6D645B)),
),
],
),
);
}
}
class _ServiceTile extends StatelessWidget {
const _ServiceTile({required this.row});
final _ServiceRow row;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.72),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0x144A3C2A)),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
row.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
row.subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF6D645B),
),
),
],
),
),
_Badge(label: row.status),
],
),
);
}
}
class _Badge extends StatelessWidget {
const _Badge({required this.label});
final String label;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
decoration: BoxDecoration(
color:
label.toLowerCase().contains('running') ||
label.toLowerCase().contains('ready')
? const Color(0x1A2C8F57)
: const Color(0x1AE3A93A),
borderRadius: BorderRadius.circular(999),
),
child: Text(
label == 'Running' ? '● Running' : label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color:
label.toLowerCase().contains('running') ||
label.toLowerCase().contains('ready')
? const Color(0xFF2C8F57)
: const Color(0xFF946600),
),
),
);
}
}
TableRow _headerRow() {
return const TableRow(
children: [
_HeaderCell('Model'),
_HeaderCell('Status'),
_HeaderCell('Latency'),
_HeaderCell('RPM'),
_HeaderCell('Cost'),
],
);
}
TableRow _modelRow(
String model,
String status,
String latency,
String rpm,
String cost,
) {
return TableRow(
children: [
_TableCell(
Text(
model,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
),
),
_TableCell(_Badge(label: status)),
_TableCell(Text(latency)),
_TableCell(Text(rpm)),
_TableCell(Text(cost)),
],
);
}
class _HeaderCell extends StatelessWidget {
const _HeaderCell(this.label);
final String label;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
label,
style: const TextStyle(
color: Color(0xFF6D645B),
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 1.1,
),
),
);
}
}
class _TableCell extends StatelessWidget {
const _TableCell(this.child);
final Widget child;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: child,
);
}
}
class _ShellLabels {
static String label(ConsolePage page) {
return switch (page) {
ConsolePage.workspace => 'Workspace',
ConsolePage.openclaw => 'OpenClaw',
ConsolePage.litellm => 'LiteLLM',
ConsolePage.vault => 'Vault',
ConsolePage.terminal => 'Terminal',
};
}
}
class _MetricData {
const _MetricData(this.label, this.value, this.subtext);
final String label;
final String value;
final String subtext;
}
class _ServiceRow {
const _ServiceRow(this.title, this.subtitle, this.status);
final String title;
final String subtitle;
final String status;
}