feat(studio): cached/Regenerate UX in audit Explain (v0.16.0)

Audit drilldown's explanation panel now surfaces whether the
answer came from the cache:
- "cached" pill next to the SYSTEM AI label, tooltip shows
  the original generation date + hit count.
- Latency renders as "orig 47000 ms" so the operator sees
  what a live call would have cost.
- Refresh icon next to the latency triggers Regenerate —
  drops the entry, re-asks the model.

System-AI editor shows a cache-status row when the cache is
non-empty with a one-click Clear button. Hidden on fresh
installs to keep the dialog quiet.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-07 17:29:36 +02:00
parent b32b4b5bda
commit d82738e17e
6 changed files with 161 additions and 9 deletions

View file

@ -119,6 +119,7 @@ class HubService {
model: r.model,
privacyMode: r.privacyMode,
apiKeyEnv: r.apiKeyEnv,
cacheCount: r.cacheCount.toInt(),
);
}
@ -126,15 +127,29 @@ class HubService {
/// answer + latency on success or an [AskAiResult] with
/// `errorKind` set on failure (never throws). Studio maps
/// the error kind to its inline-fix copy.
Future<AskAiResult> askAi(String prompt) async {
final r = await _client.askAi(prompt);
Future<AskAiResult> askAi(String prompt, {bool forceFresh = false}) async {
final r = await _client.askAi(prompt, forceFresh: forceFresh);
return AskAiResult(
errorKind: r.errorKind,
text: r.text,
latencyMs: r.latencyMs,
cached: r.cached,
cachedAt: r.cachedAt,
cacheHits: r.cacheHits,
);
}
/// Drop every cached System-AI explanation. Returns the count
/// of entries that were purged.
Future<int> clearSystemLlmCache() {
return _client.clearSystemLlmCache();
}
/// Forget a single cached entry by prompt (for "Regenerate").
Future<void> forgetCachedExplanation(String prompt) {
return _client.forgetCachedExplanation(prompt);
}
/// Persist a new System-AI configuration. The hub writes back
/// to `~/.fai/config.yaml` and hot-reloads the in-memory copy
/// in one round-trip; the caller gets the resulting status
@ -160,6 +175,7 @@ class HubService {
model: r.model,
privacyMode: r.privacyMode,
apiKeyEnv: r.apiKeyEnv,
cacheCount: r.cacheCount.toInt(),
);
}
@ -687,6 +703,10 @@ class SystemAiStatus {
/// off / redacted / full.
final String privacyMode;
final String apiKeyEnv;
/// Number of cached System-AI explanations currently stored.
/// Surfaced in Doctor + Settings so operators see the cache
/// earning its keep.
final int cacheCount;
const SystemAiStatus({
required this.enabled,
@ -695,6 +715,7 @@ class SystemAiStatus {
required this.model,
required this.privacyMode,
required this.apiKeyEnv,
this.cacheCount = 0,
});
}
@ -705,13 +726,25 @@ class AskAiResult {
/// On success: the assistant's reply.
/// On failure: a human-readable diagnostic.
final String text;
/// Round-trip latency in ms (success only).
/// Round-trip latency in ms (success only). For cache hits
/// this is the *original* generation latency.
final int latencyMs;
/// True iff the answer came from the persistent cache.
final bool cached;
/// ISO-8601 timestamp the cached entry was first generated.
/// Empty for fresh answers.
final String cachedAt;
/// How many times this entry has been served from cache.
/// 0 for fresh answers.
final int cacheHits;
const AskAiResult({
required this.errorKind,
required this.text,
required this.latencyMs,
this.cached = false,
this.cachedAt = '',
this.cacheHits = 0,
});
bool get isSuccess => errorKind.isEmpty;