feat(studio): first-run UX, recovery affordances, l10n, bundled fonts

- Connection-aware Welcome: when the hub is down, show a hero with a
  primary "Start hub" CTA + install fallback instead of a dead,
  all-unchecked onboarding checklist (the first-run cliff).
- Actionable binary-not-found (file picker + install link, not a
  "set FAI_BIN" dead end) and a connect-failure banner after
  repeated failed health polls.
- Localize six hardcoded English error/toast clusters (DE+EN ARB).
- Bundle Inter + JetBrains Mono as assets; drop the runtime
  google_fonts fetch (air-gap / KRITIS safe, no font-swap flash).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-11 23:52:18 +02:00
parent 7511867774
commit 5313266cc4
25 changed files with 1231 additions and 234 deletions

View file

@ -86,6 +86,26 @@ class _ProviderPreset {
static _ProviderPreset byWire(String wire) {
return all.firstWhere((p) => p.wire == wire, orElse: () => all.first);
}
/// Localized help body for this provider. Falls back to the
/// const English [description] for any future wire that has no
/// ARB key yet.
String descriptionFor(AppLocalizations l) {
switch (wire) {
case 'ollama':
return l.providerDescOllama;
case 'openai':
return l.providerDescOpenai;
case 'lmstudio':
return l.providerDescLmstudio;
case 'vllm':
return l.providerDescVllm;
case 'custom':
return l.providerDescCustom;
default:
return description;
}
}
}
class FaiSystemAiEditor extends StatefulWidget {
@ -372,7 +392,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
_ProviderDropdown(value: _preset, onChanged: _onProviderChanged),
const SizedBox(height: 4),
Text(
_preset.description,
_preset.descriptionFor(l),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
@ -634,10 +654,10 @@ class _TestResultPanel extends StatelessWidget {
ok ? l.systemAiReplyPrefix(result.text) : result.text,
style: FaiTheme.mono(size: 11, color: theme.colorScheme.onSurface),
),
if (!ok && result.fixHint.isNotEmpty) ...[
if (!ok && result.fixHint(l).isNotEmpty) ...[
const SizedBox(height: FaiSpace.xs),
Text(
result.fixHint,
result.fixHint(l),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface,
),