feat(studio): rich theme picker (presets + custom colour) + editor 0.11.0
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
Settings dialog's Theme Plugin section is now a grid of swatched tiles: - Built-in (none) — falls back to FaiTheme.light/.dark - One tile per installed studio.theme.* plugin, each showing the plugin's primary/secondary/tertiary as live colour dots. Tile loads its preview lazily so a dozen installed themes don't block the picker. - Custom — opens a colour-picker dialog with 12 curated Material presets + a hex input + live preview. Selecting applies ColorScheme.fromSeed for both brightnesses. main.dart's _pluginThemes parses a 'custom:#RRGGBB' sigil in the same notifier slot as plugin capability ids, so the existing persistence + restoration paths cover the custom case with no new state. Bumps editor to 0.11.0 (type-checked port connections + dynamic card width fix + card-height border allowance) and Studio to 0.58.0. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
69864da934
commit
c1c60d5434
30 changed files with 1280 additions and 846 deletions
|
|
@ -83,10 +83,7 @@ class _ProviderPreset {
|
|||
];
|
||||
|
||||
static _ProviderPreset byWire(String wire) {
|
||||
return all.firstWhere(
|
||||
(p) => p.wire == wire,
|
||||
orElse: () => all.first,
|
||||
);
|
||||
return all.firstWhere((p) => p.wire == wire, orElse: () => all.first);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,9 +138,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
|||
);
|
||||
_model = TextEditingController(text: init.model);
|
||||
_apiKeyEnv = TextEditingController(
|
||||
text: init.apiKeyEnv.isEmpty
|
||||
? _preset.defaultApiKeyEnv
|
||||
: init.apiKeyEnv,
|
||||
text: init.apiKeyEnv.isEmpty ? _preset.defaultApiKeyEnv : init.apiKeyEnv,
|
||||
);
|
||||
_privacyMode = init.privacyMode.isEmpty ? 'redacted' : init.privacyMode;
|
||||
// Pull the model list immediately so the operator sees the
|
||||
|
|
@ -376,10 +371,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
|||
_HardwareBanner(hw: _hw!, lastReviewed: _curated?.lastReviewed),
|
||||
],
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_ProviderDropdown(
|
||||
value: _preset,
|
||||
onChanged: _onProviderChanged,
|
||||
),
|
||||
_ProviderDropdown(value: _preset, onChanged: _onProviderChanged),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_preset.description,
|
||||
|
|
@ -495,10 +487,7 @@ class _ProviderDropdown extends StatelessWidget {
|
|||
final _ProviderPreset value;
|
||||
final void Function(_ProviderPreset) onChanged;
|
||||
|
||||
const _ProviderDropdown({
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
const _ProviderDropdown({required this.value, required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -512,12 +501,7 @@ class _ProviderDropdown extends StatelessWidget {
|
|||
isDense: true,
|
||||
),
|
||||
items: _ProviderPreset.all
|
||||
.map(
|
||||
(p) => DropdownMenuItem(
|
||||
value: p.wire,
|
||||
child: Text(p.label),
|
||||
),
|
||||
)
|
||||
.map((p) => DropdownMenuItem(value: p.wire, child: Text(p.label)))
|
||||
.toList(),
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
|
|
@ -531,10 +515,7 @@ class _PrivacyModeChips extends StatelessWidget {
|
|||
final String value;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
const _PrivacyModeChips({
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
const _PrivacyModeChips({required this.value, required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -547,11 +528,7 @@ class _PrivacyModeChips extends StatelessWidget {
|
|||
l.systemAiPrivacyRedactedLabel,
|
||||
l.systemAiPrivacyRedactedDesc,
|
||||
),
|
||||
(
|
||||
'full',
|
||||
l.systemAiPrivacyFullLabel,
|
||||
l.systemAiPrivacyFullDesc,
|
||||
),
|
||||
('full', l.systemAiPrivacyFullLabel, l.systemAiPrivacyFullDesc),
|
||||
];
|
||||
// Material 3 deprecated per-Radio `groupValue` / `onChanged`
|
||||
// after Flutter 3.32. The modern pattern wraps the radios in
|
||||
|
|
@ -643,9 +620,7 @@ class _TestResultPanel extends StatelessWidget {
|
|||
const SizedBox(width: FaiSpace.xs),
|
||||
Text(
|
||||
ok ? l.systemAiConnectionOk : l.systemAiConnectionFailed,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: color,
|
||||
),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: color),
|
||||
),
|
||||
const Spacer(),
|
||||
if (ok && result.latencyMs > 0)
|
||||
|
|
@ -659,10 +634,7 @@ class _TestResultPanel extends StatelessWidget {
|
|||
const SizedBox(height: 4),
|
||||
SelectableText(
|
||||
ok ? l.systemAiReplyPrefix(result.text) : result.text,
|
||||
style: FaiTheme.mono(
|
||||
size: 11,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
style: FaiTheme.mono(size: 11, color: theme.colorScheme.onSurface),
|
||||
),
|
||||
if (!ok && result.fixHint.isNotEmpty) ...[
|
||||
const SizedBox(height: FaiSpace.xs),
|
||||
|
|
@ -686,12 +658,14 @@ class _ModelPicker extends StatelessWidget {
|
|||
final String? modelsError;
|
||||
final bool loading;
|
||||
final bool pulling;
|
||||
|
||||
/// Detected host hardware. Used by [_suitabilityFor] to mark
|
||||
/// curated models as "recommended" when their `min_hw_tier` is
|
||||
/// at or below the operator's tier. Null while the hub call is
|
||||
/// in flight or unreachable — chips fall back to the size-only
|
||||
/// heuristic.
|
||||
final HardwareSnapshot? hw;
|
||||
|
||||
/// Curated DB indexed by model id. Empty while the hub call is
|
||||
/// in flight; chips then fall back to the size-only heuristic.
|
||||
final Map<String, CuratedModelInfo> curatedById;
|
||||
|
|
@ -736,8 +710,8 @@ class _ModelPicker extends StatelessWidget {
|
|||
: preset.modelHint,
|
||||
helperText: controller.text.trim().isEmpty
|
||||
? (_isOllama
|
||||
? l.systemAiModelHelperOllama
|
||||
: l.systemAiModelHelperBase)
|
||||
? l.systemAiModelHelperOllama
|
||||
: l.systemAiModelHelperBase)
|
||||
: null,
|
||||
helperMaxLines: 2,
|
||||
border: const OutlineInputBorder(),
|
||||
|
|
@ -787,9 +761,7 @@ class _ModelPicker extends StatelessWidget {
|
|||
const SizedBox(height: FaiSpace.sm),
|
||||
if (models!.isEmpty)
|
||||
Text(
|
||||
_isOllama
|
||||
? l.systemAiNoModelsOllama
|
||||
: l.systemAiNoModelsGeneric,
|
||||
_isOllama ? l.systemAiNoModelsOllama : l.systemAiNoModelsGeneric,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -970,6 +942,7 @@ List<String> _sortedBySuitability(
|
|||
class _ModelChip extends StatelessWidget {
|
||||
final String id;
|
||||
final _Suitability suitability;
|
||||
|
||||
/// Curated entry for this id, when one exists. Drives the
|
||||
/// tooltip — operators get the editorial note ("Solid 4B
|
||||
/// all-rounder…") rather than just the suitability label.
|
||||
|
|
@ -1010,9 +983,7 @@ class _ModelChip extends StatelessWidget {
|
|||
message: _tooltipFor(context),
|
||||
child: ActionChip(
|
||||
avatar: Icon(
|
||||
suitability == _Suitability.recommended
|
||||
? Icons.star
|
||||
: Icons.circle,
|
||||
suitability == _Suitability.recommended ? Icons.star : Icons.circle,
|
||||
size: suitability == _Suitability.recommended ? 14 : 9,
|
||||
color: color,
|
||||
),
|
||||
|
|
@ -1041,10 +1012,7 @@ class _HardwareBanner extends StatelessWidget {
|
|||
? l.systemAiHwReviewed(lastReviewed!)
|
||||
: '';
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: FaiSpace.sm,
|
||||
vertical: 6,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.sm, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||
|
|
@ -1081,23 +1049,25 @@ class _SuitabilityLegend extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
Widget dot(Color c, String label) => Padding(
|
||||
padding: const EdgeInsets.only(right: FaiSpace.md),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.circle, size: 8, color: c),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.only(right: FaiSpace.md),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.circle, size: 8, color: c),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
);
|
||||
final tierTag = (hw == null || hw!.tier == 'unknown') ? '' : ' (${hw!.tier})';
|
||||
],
|
||||
),
|
||||
);
|
||||
final tierTag = (hw == null || hw!.tier == 'unknown')
|
||||
? ''
|
||||
: ' (${hw!.tier})';
|
||||
return Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
|
|
@ -1143,10 +1113,7 @@ class _CacheStatusRow extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: FaiSpace.sm,
|
||||
vertical: 6,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: FaiSpace.sm, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||
|
|
@ -1154,11 +1121,7 @@ class _CacheStatusRow extends StatelessWidget {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bolt,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Icon(Icons.bolt, size: 14, color: theme.colorScheme.onSurfaceVariant),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue