feat(studio): central error helpers + inline log viewer + Today CTAs
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
Bundles the error-UX overhaul and the inline log viewer:
- New `FaiLog` (`~/.fai/logs/studio-errors.log`, 256 KiB rotation,
JSON-per-line). Every operator-visible failure is appended so
`fai admin doctor` and the new viewer can show the trail
without the operator having to reproduce the failure.
- New `showFaiErrorSnack` / `showFaiErrorDialog` helpers wrap
`FaiErrorBox` in copyable surfaces; 27 ad-hoc
`SnackBar(content: Text(e.toString()))` sites swept to use
them (settings, doctor, audit, store, module sheet, system-AI
editor, flow output).
- New `FaiLogViewer` modal (`showFaiLogViewer`) renders log
files inline with line numbers, JSON-key + `[level]` token
colouring, Copy-all, Refresh, Open-externally. Doctor's
daemon-paths panel grows a "View" button next to "Open" for
every `.log` row and now also lists the Studio errors log.
- Today carousel: CTAs now actually re-run search after a
`filterCategory` / `runQuery` story is tapped (was only
flipping the chip state). Fallback story list bumped to 8.
- Editor bumped to git ref carrying 0.15.0 (type-token
colouring + analyzer diagnostics).
Studio bumped to 0.62.0.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
2731062a13
commit
8f5cd2528b
22 changed files with 1087 additions and 134 deletions
|
|
@ -19,6 +19,7 @@ import 'package:file_picker/file_picker.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||
|
||||
import '../data/error_presentation.dart';
|
||||
import '../data/flow_output.dart';
|
||||
import '../data/format.dart';
|
||||
import '../data/system_actions.dart';
|
||||
|
|
@ -172,9 +173,7 @@ class _BytesView extends StatelessWidget {
|
|||
).showSnackBar(SnackBar(content: Text(l.flowsOutputSavedAt(path))));
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l.flowsOutputSaveFailed('$e'))));
|
||||
showFaiErrorSnack(context, 'flows.output.save', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,10 +234,11 @@ class _FileView extends StatelessWidget {
|
|||
final target = uri.startsWith('file://') ? uri.substring(7) : uri;
|
||||
final r = await SystemActions.openInOs(target);
|
||||
if (!context.mounted || r.ok) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(
|
||||
showFaiErrorSnack(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l.flowsOutputOpenFailed(r.stderr))));
|
||||
'flows.output.open',
|
||||
r.stderr.isEmpty ? 'open failed' : r.stderr,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
442
lib/widgets/fai_log_viewer.dart
Normal file
442
lib/widgets/fai_log_viewer.dart
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
// FaiLogViewer — modal sheet that renders a log file inline with
|
||||
// line numbers, basic syntax colouring and a Copy-all button.
|
||||
//
|
||||
// Use [showFaiLogViewer] to open it. Two log shapes are supported
|
||||
// out of the box:
|
||||
//
|
||||
// - JSON-per-line (Studio's `~/.fai/logs/studio-errors.log`):
|
||||
// fields `ts`, `level`, `source`, `error` get distinct colours;
|
||||
// anything else renders neutral.
|
||||
// - tracing text (the daemon's `~/.fai/run/<channel>.log`):
|
||||
// `[INFO]`, `[WARN]`, `[ERROR]`, `[DEBUG]` substrings get the
|
||||
// matching tone from the FaiTheme palette.
|
||||
//
|
||||
// The viewer reads the *last* N lines (default 500) to keep
|
||||
// rendering snappy on huge log files. A Refresh button re-reads
|
||||
// the file in place. An "Open externally" button still hands the
|
||||
// path to the OS so power users can pipe it through their tool
|
||||
// of choice.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../data/system_actions.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
import '../theme/tokens.dart';
|
||||
|
||||
/// Open [path] inside a modal log viewer.
|
||||
///
|
||||
/// [title] is shown in the AppBar (e.g. `"local.log"` or
|
||||
/// `"studio-errors.log"`); falls back to the basename of [path].
|
||||
Future<void> showFaiLogViewer(
|
||||
BuildContext context, {
|
||||
required String path,
|
||||
String? title,
|
||||
int tailLines = 500,
|
||||
}) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (_) => FaiLogViewer(
|
||||
path: path,
|
||||
title: title ?? path.split(Platform.pathSeparator).last,
|
||||
tailLines: tailLines,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class FaiLogViewer extends StatefulWidget {
|
||||
final String path;
|
||||
final String title;
|
||||
final int tailLines;
|
||||
|
||||
const FaiLogViewer({
|
||||
super.key,
|
||||
required this.path,
|
||||
required this.title,
|
||||
this.tailLines = 500,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FaiLogViewer> createState() => _FaiLogViewerState();
|
||||
}
|
||||
|
||||
class _FaiLogViewerState extends State<FaiLogViewer> {
|
||||
List<String> _lines = const <String>[];
|
||||
bool _loading = true;
|
||||
bool _justCopied = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_reload();
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
setState(() => _loading = true);
|
||||
final lines = await _readTail(widget.path, widget.tailLines);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_lines = lines;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _copyAll() async {
|
||||
await Clipboard.setData(ClipboardData(text: _lines.join('\n')));
|
||||
if (!mounted) return;
|
||||
setState(() => _justCopied = true);
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) setState(() => _justCopied = false);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _openExternal() async {
|
||||
await SystemActions.openOrReveal(widget.path, isDirectory: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: FaiSpace.xl,
|
||||
vertical: FaiSpace.xl,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 980, maxHeight: 720),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_Header(
|
||||
title: widget.title,
|
||||
path: widget.path,
|
||||
lineCount: _lines.length,
|
||||
tailLimit: widget.tailLines,
|
||||
loading: _loading,
|
||||
justCopied: _justCopied,
|
||||
onCopy: _copyAll,
|
||||
onReload: _reload,
|
||||
onOpenExternal: _openExternal,
|
||||
onClose: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Divider(height: 1, color: theme.colorScheme.outlineVariant),
|
||||
Expanded(
|
||||
child: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _lines.isEmpty
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||
child: Text(
|
||||
l.logViewerEmpty,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: _LogBody(lines: _lines),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
final String title;
|
||||
final String path;
|
||||
final int lineCount;
|
||||
final int tailLimit;
|
||||
final bool loading;
|
||||
final bool justCopied;
|
||||
final VoidCallback onCopy;
|
||||
final VoidCallback onReload;
|
||||
final VoidCallback onOpenExternal;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _Header({
|
||||
required this.title,
|
||||
required this.path,
|
||||
required this.lineCount,
|
||||
required this.tailLimit,
|
||||
required this.loading,
|
||||
required this.justCopied,
|
||||
required this.onCopy,
|
||||
required this.onReload,
|
||||
required this.onOpenExternal,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
FaiSpace.lg,
|
||||
FaiSpace.md,
|
||||
FaiSpace.sm,
|
||||
FaiSpace.md,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.description_outlined,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
l.logViewerTitle(title, tailLimit),
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'$path · ${l.logViewerLineCount(lineCount)}',
|
||||
style: FaiTheme.mono(
|
||||
size: 10,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: justCopied ? l.buttonCopied : l.logViewerCopyAll,
|
||||
icon: Icon(
|
||||
justCopied ? Icons.check : Icons.content_copy,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: loading || lineCount == 0 ? null : onCopy,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l.buttonRefresh,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
onPressed: loading ? null : onReload,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l.logViewerOpenExternal,
|
||||
icon: const Icon(Icons.open_in_new, size: 16),
|
||||
onPressed: onOpenExternal,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l.buttonClose,
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
onPressed: onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LogBody extends StatelessWidget {
|
||||
final List<String> lines;
|
||||
|
||||
const _LogBody({required this.lines});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final gutterWidth = 12.0 + 8.0 * lines.length.toString().length;
|
||||
return Scrollbar(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm),
|
||||
itemCount: lines.length,
|
||||
itemBuilder: (context, i) => _LogLine(
|
||||
lineNumber: i + 1,
|
||||
text: lines[i],
|
||||
gutterWidth: gutterWidth,
|
||||
theme: theme,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LogLine extends StatelessWidget {
|
||||
final int lineNumber;
|
||||
final String text;
|
||||
final double gutterWidth;
|
||||
final ThemeData theme;
|
||||
|
||||
const _LogLine({
|
||||
required this.lineNumber,
|
||||
required this.text,
|
||||
required this.gutterWidth,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.sm, vertical: 1),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: gutterWidth,
|
||||
child: Text(
|
||||
'$lineNumber',
|
||||
textAlign: TextAlign.right,
|
||||
style: FaiTheme.mono(
|
||||
size: 11,
|
||||
color: theme.colorScheme.onSurfaceVariant.withValues(
|
||||
alpha: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Expanded(
|
||||
child: SelectableText.rich(
|
||||
_highlightLine(text, theme),
|
||||
style: FaiTheme.mono(
|
||||
size: 11,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lightweight tokeniser shared by both supported log shapes.
|
||||
///
|
||||
/// We don't reach for a full grammar here on purpose: log files
|
||||
/// are short-lived, the operator just needs the eye to land on
|
||||
/// the level/source quickly. Real syntax highlighting belongs in
|
||||
/// the flow editor; logs only need three or four accent colours.
|
||||
TextSpan _highlightLine(String line, ThemeData theme) {
|
||||
// JSON-per-line path (the Studio error log). Looks like:
|
||||
// {"ts":"…","level":"error","source":"flows.run","error":"…"}
|
||||
if (line.startsWith('{') && line.endsWith('}')) {
|
||||
return _highlightJsonLine(line, theme);
|
||||
}
|
||||
return _highlightTraceLine(line, theme);
|
||||
}
|
||||
|
||||
TextSpan _highlightJsonLine(String line, ThemeData theme) {
|
||||
final spans = <TextSpan>[];
|
||||
final base = TextStyle(color: theme.colorScheme.onSurface);
|
||||
final keyStyle = TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
final stringStyle = TextStyle(color: FaiColors.success);
|
||||
final errorStringStyle = TextStyle(color: theme.colorScheme.error);
|
||||
|
||||
final keyRe = RegExp(r'"(ts|level|source|error|context)"\s*:');
|
||||
final stringRe = RegExp(r'"((?:\\.|[^"\\])*)"');
|
||||
|
||||
int cursor = 0;
|
||||
bool nextStringIsError = false;
|
||||
while (cursor < line.length) {
|
||||
final keyMatch = keyRe.matchAsPrefix(line, cursor);
|
||||
if (keyMatch != null) {
|
||||
spans.add(TextSpan(text: keyMatch.group(0), style: keyStyle));
|
||||
nextStringIsError = keyMatch.group(1) == 'error';
|
||||
cursor = keyMatch.end;
|
||||
continue;
|
||||
}
|
||||
final stringMatch = stringRe.matchAsPrefix(line, cursor);
|
||||
if (stringMatch != null) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: stringMatch.group(0),
|
||||
style: nextStringIsError ? errorStringStyle : stringStyle,
|
||||
),
|
||||
);
|
||||
cursor = stringMatch.end;
|
||||
nextStringIsError = false;
|
||||
continue;
|
||||
}
|
||||
spans.add(TextSpan(text: line[cursor], style: base));
|
||||
cursor++;
|
||||
}
|
||||
return TextSpan(children: spans);
|
||||
}
|
||||
|
||||
TextSpan _highlightTraceLine(String line, ThemeData theme) {
|
||||
// Tracing line: prefix tag like [INFO] / [WARN] / [ERROR] /
|
||||
// [DEBUG] gets the tone colour, everything else stays neutral.
|
||||
final tagRe = RegExp(r'\[(INFO|WARN|WARNING|ERROR|DEBUG|TRACE)\]');
|
||||
final match = tagRe.firstMatch(line);
|
||||
if (match == null) {
|
||||
return TextSpan(
|
||||
text: line,
|
||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||
);
|
||||
}
|
||||
Color tone;
|
||||
switch (match.group(1)) {
|
||||
case 'ERROR':
|
||||
tone = theme.colorScheme.error;
|
||||
break;
|
||||
case 'WARN':
|
||||
case 'WARNING':
|
||||
tone = FaiColors.warning;
|
||||
break;
|
||||
case 'INFO':
|
||||
tone = theme.colorScheme.primary;
|
||||
break;
|
||||
default:
|
||||
tone = theme.colorScheme.onSurfaceVariant;
|
||||
}
|
||||
return TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: line.substring(0, match.start),
|
||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||
),
|
||||
TextSpan(
|
||||
text: match.group(0),
|
||||
style: TextStyle(color: tone, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextSpan(
|
||||
text: line.substring(match.end),
|
||||
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Read the last [maxLines] of [path]. Returns an empty list on
|
||||
/// any I/O failure — the viewer renders an empty-state hint in
|
||||
/// that case rather than throwing.
|
||||
Future<List<String>> _readTail(String path, int maxLines) async {
|
||||
try {
|
||||
final f = File(path);
|
||||
if (!await f.exists()) return const <String>[];
|
||||
final raw = await f.readAsString();
|
||||
final lines = raw.split('\n');
|
||||
// Drop the trailing blank line that `split` leaves behind
|
||||
// when the file ends with `\n`.
|
||||
if (lines.isNotEmpty && lines.last.isEmpty) {
|
||||
lines.removeLast();
|
||||
}
|
||||
if (lines.length <= maxLines) return lines;
|
||||
return lines.sublist(lines.length - maxLines);
|
||||
} catch (_) {
|
||||
return const <String>[];
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/error_presentation.dart';
|
||||
import '../data/hub.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
|
|
@ -104,9 +105,7 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
|||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _uninstalling = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.modulesUninstallFailed(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'modules.uninstall', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import 'package:fai_client_sdk/fai_client_sdk.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/error_presentation.dart';
|
||||
import '../data/hub.dart';
|
||||
import '../data/hub_auth_token.dart';
|
||||
import '../data/registry_token.dart';
|
||||
|
|
@ -92,10 +93,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
).showSnackBar(SnackBar(content: Text(l.hubAuthTokenSavedToast)));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.hubAuthTokenSaveFailedToast(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'auth.hub-token.save', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +109,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
).showSnackBar(SnackBar(content: Text(l.hubAuthTokenClearedToast)));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.hubAuthTokenSaveFailedToast(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'auth.hub-token.clear', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +134,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
).showSnackBar(SnackBar(content: Text(l.registryTokenSavedToast)));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.registryTokenSaveFailedToast(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'auth.registry-token.save', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,10 +149,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
).showSnackBar(SnackBar(content: Text(l.registryTokenClearedToast)));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.registryTokenSaveFailedToast(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'auth.registry-token.clear', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,10 +179,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
).showSnackBar(SnackBar(content: Text(l.addedN8nToast(draft.name))));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l.addFailedToast(e.toString()))));
|
||||
showFaiErrorSnack(context, 'settings.n8n.add', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,10 +190,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
setState(() => _n8nEndpoints = list);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.removeFailedToast(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'settings.n8n.remove', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,10 +201,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
setState(() => _n8nEndpoints = list);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.refreshFailedToast(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'settings.n8n.refresh', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,10 +231,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
).showSnackBar(SnackBar(content: Text(l.addedMcpToast(draft.name))));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l.addFailedToast(e.toString()))));
|
||||
showFaiErrorSnack(context, 'settings.mcp.add', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,10 +242,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
setState(() => _mcpClients = list);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.removeFailedToast(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'settings.mcp.remove', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,10 +253,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
setState(() => _mcpClients = list);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.refreshFailedToast(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'settings.mcp.refresh', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/error_presentation.dart';
|
||||
import '../data/hub.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
|
|
@ -249,10 +250,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
|||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.systemAiCacheClearFailed(e.toString()))),
|
||||
);
|
||||
showFaiErrorSnack(context, 'settings.system-ai.cache-clear', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ class _Tile extends StatelessWidget {
|
|||
final List<Color> swatches;
|
||||
final bool selected;
|
||||
final bool loading;
|
||||
|
||||
/// Adds a small tune icon at the top-right, telegraphing
|
||||
/// "this tile opens an editor instead of applying
|
||||
/// immediately". Used for the Custom tile so the operator
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export 'fai_empty_state.dart';
|
|||
export 'fai_en_badge.dart';
|
||||
export 'fai_error_box.dart';
|
||||
export 'fai_flow_output.dart';
|
||||
export 'fai_log_viewer.dart';
|
||||
export 'fai_module_sheet.dart';
|
||||
export 'fai_pill.dart';
|
||||
export 'fai_settings_dialog.dart';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue