feat(studio): MCP suggestions + Approvals/Audit/Settings i18n (v0.26.0)
- Add curated catalogue of 9 official Anthropic MCP servers as click-to-prefill chips in the Add-MCP-server dialog. Operator still confirms via "Add + discover"; nothing is auto-spawned, preserving the regulated-environment trust model. - Localize Approvals page: pending/history empty states, status pills, payload preview, approve/reject toasts, reject dialog, history detail dialog, expires-in pill. - Localize Audit page: filter chips, clear-log dialog and toasts, no-events / hub-unreachable empty states, live-status bar with pluralized event count, hash-chain-verified badge, System AI panel (cached pill, latency, regenerate tooltip, asking state), event-detail dialog actions. - Localize Settings dialog: hub-endpoint section, channel blurb + popup-menu items, System AI panel headers and off-blurb, MCP/N8N panel headers + pluralized counts + refresh/add/remove tooltips, Add-MCP and Add-n8n dialogs (intro, suggestions label, all field labels and hints, save buttons, toasts). Field labels that map directly to JSON keys (event_id, flow, step, module, provider, endpoint, model) stay English by design — they're technical identifiers, not UI copy. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
349619d68e
commit
d25b4c87ae
9 changed files with 2022 additions and 209 deletions
|
|
@ -41,6 +41,7 @@ class _AuditPageState extends State<AuditPage> {
|
|||
}
|
||||
|
||||
Future<void> _onClearPressed() async {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final outcome = await _ClearAuditDialog.show(context);
|
||||
if (outcome == null || !mounted) return;
|
||||
try {
|
||||
|
|
@ -50,18 +51,13 @@ class _AuditPageState extends State<AuditPage> {
|
|||
);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Cleared ${r.purged} events on channel "${r.channel}". '
|
||||
'Chain reseeded with marker.',
|
||||
),
|
||||
),
|
||||
SnackBar(content: Text(l.auditClearedToast(r.purged, r.channel))),
|
||||
);
|
||||
_refresh();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Clear failed: ${_friendly(e)}')),
|
||||
SnackBar(content: Text(l.auditClearFailed(_friendly(e)))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -115,7 +111,7 @@ class _AuditPageState extends State<AuditPage> {
|
|||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_sweep_outlined, size: 18),
|
||||
tooltip: 'Clear log (local/dev channels only)',
|
||||
tooltip: AppLocalizations.of(context)!.auditClearLogTooltip,
|
||||
onPressed: _onClearPressed,
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
|
|
@ -131,15 +127,15 @@ class _AuditPageState extends State<AuditPage> {
|
|||
? FaiEmptyState(
|
||||
icon: Icons.cloud_off_outlined,
|
||||
iconColor: theme.colorScheme.error,
|
||||
title: 'Hub unreachable',
|
||||
hint: 'Start the hub with `fai serve`.',
|
||||
title: AppLocalizations.of(context)!.hubUnreachable,
|
||||
hint: AppLocalizations.of(context)!.hubUnreachableHint,
|
||||
)
|
||||
: filtered.isEmpty
|
||||
? const FaiEmptyState(
|
||||
? FaiEmptyState(
|
||||
icon: Icons.timeline_outlined,
|
||||
title: 'No events yet',
|
||||
title: AppLocalizations.of(context)!.auditNoEvents,
|
||||
hint:
|
||||
'Run a flow to populate the audit stream.\nEvents appear here within seconds.',
|
||||
AppLocalizations.of(context)!.auditNoEventsHint,
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||
|
|
@ -220,11 +216,12 @@ class _FilterChips extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const items = [
|
||||
('all', 'all'),
|
||||
('flow.', 'flow'),
|
||||
('step.', 'step'),
|
||||
('module.', 'module'),
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final items = [
|
||||
('all', l.auditFilterAll),
|
||||
('flow.', l.auditFilterFlow),
|
||||
('step.', l.auditFilterStep),
|
||||
('module.', l.auditFilterModule),
|
||||
];
|
||||
return Row(
|
||||
children: [
|
||||
|
|
@ -317,6 +314,7 @@ class _LiveStatusBar extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final live = error == null;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
|
|
@ -339,8 +337,8 @@ class _LiveStatusBar extends StatelessWidget {
|
|||
const SizedBox(width: FaiSpace.sm),
|
||||
Text(
|
||||
live
|
||||
? 'live · polling 2s · $eventCount event${eventCount == 1 ? '' : 's'}'
|
||||
: 'disconnected · $error',
|
||||
? l.auditLiveStatus(eventCount)
|
||||
: l.auditDisconnected(error!),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -356,7 +354,7 @@ class _LiveStatusBar extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'hash chain verified',
|
||||
l.auditHashChainVerified,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
|
@ -445,6 +443,7 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
String prettyDetail() {
|
||||
final raw = event.detail;
|
||||
if (raw == null || raw.isEmpty) return '';
|
||||
|
|
@ -511,7 +510,7 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
|||
if (detail.isNotEmpty) ...[
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
Text(
|
||||
'DETAIL',
|
||||
l.auditDetailHeader,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
|
|
@ -556,24 +555,24 @@ class _EventDetailDialogState extends State<_EventDetailDialog> {
|
|||
icon: const Icon(Icons.auto_awesome, size: 16),
|
||||
label: Text(
|
||||
_explaining
|
||||
? 'Asking…'
|
||||
? l.auditAsking
|
||||
: _explanation == null
|
||||
? 'Explain'
|
||||
: 'Re-ask',
|
||||
? l.auditExplain
|
||||
: l.auditReask,
|
||||
),
|
||||
)
|
||||
else if (_aiStatus != null && !_aiStatus!.enabled && (event.error != null))
|
||||
Tooltip(
|
||||
message: 'Configure System AI in Settings (Cmd+,)',
|
||||
message: l.auditConfigureSystemAi,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: null,
|
||||
icon: const Icon(Icons.auto_awesome_outlined, size: 16),
|
||||
label: const Text('Explain'),
|
||||
label: Text(l.auditExplain),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
child: Text(l.buttonClose),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -648,6 +647,7 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final ok = result?.isSuccess ?? false;
|
||||
final color = explaining
|
||||
? theme.colorScheme.primary
|
||||
|
|
@ -670,7 +670,7 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
Icon(Icons.auto_awesome, size: 14, color: color),
|
||||
const SizedBox(width: FaiSpace.xs),
|
||||
Text(
|
||||
'SYSTEM AI',
|
||||
l.auditSystemAi,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
|
|
@ -686,12 +686,16 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
if (result != null && result!.isSuccess && result!.cached) ...[
|
||||
const SizedBox(width: FaiSpace.xs),
|
||||
Tooltip(
|
||||
message:
|
||||
'Served from the local cache. Originally generated '
|
||||
'${result!.cachedAt.isEmpty ? "at unknown time" : "on ${result!.cachedAt}"}'
|
||||
'${result!.cacheHits > 1 ? " · ${result!.cacheHits} hits" : ""}.',
|
||||
child: const FaiPill(
|
||||
label: 'cached',
|
||||
message: () {
|
||||
final hits = result!.cacheHits > 1
|
||||
? l.auditCachedTooltipHits(result!.cacheHits)
|
||||
: '';
|
||||
return result!.cachedAt.isEmpty
|
||||
? l.auditCachedTooltipUnknown(hits)
|
||||
: l.auditCachedTooltipKnown(result!.cachedAt, hits);
|
||||
}(),
|
||||
child: FaiPill(
|
||||
label: l.auditCachedPill,
|
||||
tone: FaiPillTone.success,
|
||||
icon: Icons.bolt,
|
||||
),
|
||||
|
|
@ -701,8 +705,8 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
if (result != null && result!.isSuccess && result!.latencyMs > 0)
|
||||
Text(
|
||||
result!.cached
|
||||
? 'orig ${result!.latencyMs} ms'
|
||||
: '${result!.latencyMs} ms',
|
||||
? l.auditOriginalLatency(result!.latencyMs)
|
||||
: l.auditLatency(result!.latencyMs),
|
||||
style: FaiTheme.mono(
|
||||
size: 10,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
|
|
@ -711,7 +715,7 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
if (result != null && result!.isSuccess && onRegenerate != null) ...[
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Tooltip(
|
||||
message: 'Regenerate — skip cache, ask the model again',
|
||||
message: l.auditRegenerateTooltip,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 14),
|
||||
visualDensity: VisualDensity.compact,
|
||||
|
|
@ -732,7 +736,7 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Text(
|
||||
'Asking the configured System AI…',
|
||||
l.auditAskingFull,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -749,7 +753,7 @@ class _ExplanationPanel extends StatelessWidget {
|
|||
if (result!.fixHint.isNotEmpty) ...[
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Text(
|
||||
'FIX',
|
||||
l.auditFixLabel,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
|
|
@ -820,8 +824,9 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return AlertDialog(
|
||||
title: const Text('Clear audit log?'),
|
||||
title: Text(l.auditClearDialogTitle),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 460),
|
||||
child: Column(
|
||||
|
|
@ -829,9 +834,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Wipes every audit event on the active channel and seeds a '
|
||||
'fresh chain.reset marker carrying reviewer + reason. '
|
||||
'Refused on beta / production. Irreversible.',
|
||||
l.auditClearDialogBody,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -839,9 +842,9 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
const SizedBox(height: FaiSpace.lg),
|
||||
TextField(
|
||||
controller: _reviewer,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Reviewer',
|
||||
border: OutlineInputBorder(),
|
||||
decoration: InputDecoration(
|
||||
labelText: l.auditClearReviewerLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
|
|
@ -852,9 +855,9 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
controller: _reason,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Reason (recorded in chain.reset marker)',
|
||||
labelText: l.auditClearReasonLabel,
|
||||
helperText: _reason.text.trim().isEmpty
|
||||
? 'Required — auditors will read this.'
|
||||
? l.auditClearReasonHelper
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
|
|
@ -867,7 +870,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, null),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _reason,
|
||||
|
|
@ -885,7 +888,7 @@ class _ClearAuditDialogState extends State<_ClearAuditDialog> {
|
|||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
child: const Text('Clear log'),
|
||||
child: Text(l.auditClearLogButton),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue