feat(studio): hardware-aware model recommendations + sidebar clock (v0.14.0)

System-AI editor now fetches the detected hardware tier and the
hub's curated model database on open. Each model chip's colour
+ tooltip combines both: green star when curated quality is
good/excellent AND min_hw_tier ≤ host; orange when quality is
fine but the host is below min_hw_tier; warning for "basic"
curated entries. Tooltip exposes the curator's note, parameter
count, context window, and licence. Banner above the form
shows "Detected: <summary> · curation reviewed <date>".

Sidebar footer gains a small monospaced live clock between the
theme toggle and the settings icon. Tooltip exposes full local
ISO + UTC for cross-checking against audit-log timestamps.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-07 15:39:27 +02:00
parent a83a138259
commit 942c29a395
5 changed files with 395 additions and 35 deletions

View file

@ -230,6 +230,46 @@ class HubService {
);
}
/// Detected host hardware. Used by the System-AI editor to
/// mark models as "recommended" / "may be slow" against the
/// operator's actual machine.
Future<HardwareSnapshot> hardwareInfo() async {
final r = await _client.hardwareInfo();
return HardwareSnapshot(
tier: r.tier,
cpuBrand: r.cpuBrand,
cpuCores: r.cpuCores,
ramMb: r.ramMb.toInt(),
appleSilicon: r.appleSilicon,
dedicatedGpu: r.dedicatedGpu,
summary: r.summary,
);
}
/// Bundled curated model database. Refresh requires a hub
/// redeploy; the `lastReviewed` field shows how stale the
/// curation is.
Future<CuratedSnapshot> listSystemAiCuratedModels() async {
final r = await _client.listSystemAiCuratedModels();
return CuratedSnapshot(
lastReviewed: r.lastReviewed,
models: r.models
.map(
(m) => CuratedModelInfo(
id: m.id,
family: m.family,
paramsB: m.paramsB,
qualityTier: m.qualityTier,
minHwTier: m.minHwTier,
contextK: m.contextK,
notes: m.notes,
license: m.license,
),
)
.toList(),
);
}
/// Active channel + per-channel daemon status snapshot.
Future<ChannelStatusSnapshot> channelStatus() async {
final r = await _client.channelStatus();
@ -707,6 +747,103 @@ class ChannelStatusSnapshot {
});
}
/// Detected host hardware see
/// docs/architecture/system-ai.md "Hardware tiers".
class HardwareSnapshot {
/// One of: tiny / small / balanced / large / unknown.
final String tier;
final String cpuBrand;
final int cpuCores;
final int ramMb;
final bool appleSilicon;
final bool dedicatedGpu;
/// Human-readable summary the editor renders verbatim.
final String summary;
const HardwareSnapshot({
required this.tier,
required this.cpuBrand,
required this.cpuCores,
required this.ramMb,
required this.appleSilicon,
required this.dedicatedGpu,
required this.summary,
});
/// Numeric rank used for "is `min_hw_tier` ≤ detected?"
/// comparisons. Unknown sits at -1 so it never qualifies as
/// "at least" anything; the editor falls back to size-only
/// heuristics in that case.
int get rank {
switch (tier) {
case 'tiny':
return 0;
case 'small':
return 1;
case 'balanced':
return 2;
case 'large':
return 3;
default:
return -1;
}
}
}
/// One curated model with editorial metadata. Wire-faithful
/// with `crates/fai_hub/system-ai/models.yaml`.
class CuratedModelInfo {
final String id;
final String family;
final double paramsB;
/// basic / good / excellent.
final String qualityTier;
/// tiny / small / balanced / large.
final String minHwTier;
final int contextK;
final String notes;
final String license;
const CuratedModelInfo({
required this.id,
required this.family,
required this.paramsB,
required this.qualityTier,
required this.minHwTier,
required this.contextK,
required this.notes,
required this.license,
});
/// Same rank as HardwareSnapshot; lets us compare with `<=`.
int get minHwRank {
switch (minHwTier) {
case 'tiny':
return 0;
case 'small':
return 1;
case 'balanced':
return 2;
case 'large':
return 3;
default:
return -1;
}
}
}
/// Curated database snapshot returned from the hub.
class CuratedSnapshot {
/// ISO date string from `models.yaml`, e.g. "2026-05-07".
final String lastReviewed;
final List<CuratedModelInfo> models;
const CuratedSnapshot({
required this.lastReviewed,
required this.models,
});
}
/// One row from the store-index search.
class StoreItem {
final String name;