feat(studio): Flows + Module sheet + System-AI editor i18n (v0.28.0)
- Localize Flows page: app-bar title and reload tooltip,
hub-unreachable / no-saved-flows empty states, run-flow input
dialog (title with flow name, description, hint, Cancel /
Run), running dialog (title, "Flow running…" status, no-output
message, Close button), flow-card Run button.
- Localize the module-sheet bottom sheet: failed-to-load text,
Capabilities and "Declared permissions" section headers,
no-permissions placeholder copy.
- Localize the System-AI configuration dialog: title, intro
paragraph, provider dropdown label, endpoint label, API-key
env-var label (required vs optional) and disclaimer, privacy
mode header and three options (Off / Redacted / Full) plus
their descriptions, test-result panel ("Connection ok" /
"Connection failed", "Reply: …" prefix), model picker (label,
hint fallback, helper text with Ollama variant, Refresh /
Pull / Pulling… buttons, list errors and empty states),
hardware banner ("Detected: …" + " · curation reviewed …"
suffix), suitability legend (recommended / balanced / small /
large / huge / unknown), cache row with pluralized count and
Clear button, cache cleared / clear-failed toasts, pull-empty
/ pull-failed errors. Suitability label moved off a getter
onto a `labelFor(BuildContext)` method so it can read the
current locale.
Provider preset descriptions and modelHint strings stay in
English in `_ProviderPreset.all` — they're tightly coupled to
the wire-protocol identifiers and would require a runtime
factory rebuild rather than a const list.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
e2b2639a86
commit
b5a4c74c4b
10 changed files with 1120 additions and 91 deletions
|
|
@ -263,6 +263,25 @@
|
||||||
"modulesUninstallFailed": "Deinstallation fehlgeschlagen: {error}",
|
"modulesUninstallFailed": "Deinstallation fehlgeschlagen: {error}",
|
||||||
"@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } },
|
"@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
"modulesUninstalling": "Wird deinstalliert…",
|
"modulesUninstalling": "Wird deinstalliert…",
|
||||||
|
"moduleSheetFailedToLoad": "Laden fehlgeschlagen: {error}",
|
||||||
|
"@moduleSheetFailedToLoad": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"moduleSheetCapabilities": "Capabilities",
|
||||||
|
"moduleSheetPermissions": "Deklarierte Berechtigungen",
|
||||||
|
"moduleSheetNoPermissions": "(keine — reines Berechnungsmodul)",
|
||||||
|
|
||||||
|
"flowsTitle": "Flows",
|
||||||
|
"flowsReloadTooltip": "Aktualisieren",
|
||||||
|
"flowsNoneTitle": "Keine gespeicherten Flows",
|
||||||
|
"flowsNoneHint": "Flow speichern mit `fai admin flows save <name> <flow.yaml>`, dann taucht er hier auf.",
|
||||||
|
"flowsRunDialogTitle": "{name} ausführen",
|
||||||
|
"@flowsRunDialogTitle": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"flowsRunDialogBody": "Eingaben als key=value, eine pro Zeile. Alle Werte werden als Text-Payloads gesendet. Für binäre Eingaben die `fai run` CLI nutzen.",
|
||||||
|
"flowsInputsHint": "name=World\ntarget_language=English\nsummary_style=three bullet points",
|
||||||
|
"flowsRunButton": "Starten",
|
||||||
|
"flowsRunningTitle": "{name} läuft",
|
||||||
|
"@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"flowsRunning": "Flow läuft…",
|
||||||
|
"flowsNoOutputs": "Flow abgeschlossen ohne deklarierte Outputs.",
|
||||||
|
|
||||||
"approvalsTitle": "Freigaben",
|
"approvalsTitle": "Freigaben",
|
||||||
"approvalsTabPending": "Offen",
|
"approvalsTabPending": "Offen",
|
||||||
|
|
@ -411,5 +430,65 @@
|
||||||
"@searchStoreHintWithTagline": { "placeholders": { "tagline": { "type": "String" } } },
|
"@searchStoreHintWithTagline": { "placeholders": { "tagline": { "type": "String" } } },
|
||||||
"searchStoreHintWithCategory": "Store · {category}",
|
"searchStoreHintWithCategory": "Store · {category}",
|
||||||
"@searchStoreHintWithCategory": { "placeholders": { "category": { "type": "String" } } },
|
"@searchStoreHintWithCategory": { "placeholders": { "category": { "type": "String" } } },
|
||||||
"searchSavedFlowHint": "gespeicherter Flow"
|
"searchSavedFlowHint": "gespeicherter Flow",
|
||||||
|
|
||||||
|
"systemAiTitle": "System-AI",
|
||||||
|
"systemAiIntro": "Die hub-interne LLM, die die Plattform selbst für Fehlererklärungen und Operator-Hilfe nutzt. Standardmäßig aus; hier konfigurieren. Siehe docs/architecture/system-ai.md, was über die Leitung geht.",
|
||||||
|
"systemAiProviderLabel": "Provider",
|
||||||
|
"systemAiEndpointLabel": "Endpunkt-URL (bis /v1)",
|
||||||
|
"systemAiApiKeyRequired": "API-Key-Umgebungsvariable (erforderlich)",
|
||||||
|
"systemAiApiKeyOptional": "API-Key-Umgebungsvariable (optional)",
|
||||||
|
"systemAiApiKeyDisclaimer": "Studio liest oder speichert den Key-Wert nie — nur den Namen der Umgebungsvariable. Der Hub liest `${name}` zur Anfragezeit.",
|
||||||
|
"@systemAiApiKeyDisclaimer": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"systemAiTestConnection": "Verbindung testen",
|
||||||
|
"systemAiTesting": "Teste…",
|
||||||
|
"systemAiSave": "Speichern",
|
||||||
|
"systemAiPrivacyHeader": "PRIVACY-MODUS",
|
||||||
|
"systemAiPrivacyOffLabel": "Aus",
|
||||||
|
"systemAiPrivacyOffDesc": "Feature deaktiviert. Keine Anfragen verlassen den Hub.",
|
||||||
|
"systemAiPrivacyRedactedLabel": "Redigiert",
|
||||||
|
"systemAiPrivacyRedactedDesc": "Nur event_type / error / Modul-Namen. KRITIS-freundlicher Standard.",
|
||||||
|
"systemAiPrivacyFullLabel": "Voll",
|
||||||
|
"systemAiPrivacyFullDesc": "Enthält das Audit-Detail-JSON (nur Hash — keine Roh-Payloads).",
|
||||||
|
"systemAiConnectionOk": "Verbindung ok",
|
||||||
|
"systemAiConnectionFailed": "Verbindung fehlgeschlagen",
|
||||||
|
"systemAiReplyPrefix": "Antwort: {text}",
|
||||||
|
"@systemAiReplyPrefix": { "placeholders": { "text": { "type": "String" } } },
|
||||||
|
"systemAiModelLabel": "Modell (erforderlich)",
|
||||||
|
"systemAiModelHintFallback": "Modell-Identifier, den der Provider erwartet",
|
||||||
|
"systemAiModelHelperBase": "Erforderlich: aus der Liste auswählen (Aktualisieren) oder eintippen.",
|
||||||
|
"systemAiModelHelperOllama": "Erforderlich: aus der Liste auswählen (Aktualisieren) oder eintippen. Mit Pull von Ollama herunterladen.",
|
||||||
|
"systemAiRefresh": "Aktualisieren",
|
||||||
|
"systemAiPull": "Pull",
|
||||||
|
"systemAiPulling": "Lade…",
|
||||||
|
"systemAiCouldNotListModels": "Modelle konnten nicht aufgelistet werden: {error}",
|
||||||
|
"@systemAiCouldNotListModels": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"systemAiNoModelsOllama": "Noch keine Modelle auf dem Ollama-Server. Oben eintippen und Pull klicken.",
|
||||||
|
"systemAiNoModelsGeneric": "Provider lieferte keine Modelle.",
|
||||||
|
"systemAiSuitabilityRecommended": "empfohlen",
|
||||||
|
"systemAiSuitabilityBalanced": "ausgewogen",
|
||||||
|
"systemAiSuitabilitySmall": "klein — Qualität evtl. eingeschränkt",
|
||||||
|
"systemAiSuitabilityLarge": "groß — langsamer",
|
||||||
|
"systemAiSuitabilityHuge": "sehr groß — auf einem Laptop wahrscheinlich zu langsam",
|
||||||
|
"systemAiSuitabilityUnknown": "Größe unbekannt",
|
||||||
|
"systemAiHwDetected": "Erkannt: {summary}",
|
||||||
|
"@systemAiHwDetected": { "placeholders": { "summary": { "type": "String" } } },
|
||||||
|
"systemAiHwReviewed": " · Kuratierung geprüft {date}",
|
||||||
|
"@systemAiHwReviewed": { "placeholders": { "date": { "type": "String" } } },
|
||||||
|
"systemAiLegendRecommended": "empfohlen für diese Hardware{tier}",
|
||||||
|
"@systemAiLegendRecommended": { "placeholders": { "tier": { "type": "String" } } },
|
||||||
|
"systemAiLegendBalanced": "ausgewogen (4-8B)",
|
||||||
|
"systemAiLegendSmallLarge": "klein <3B / groß 9-15B / über Hardware",
|
||||||
|
"systemAiLegendHuge": "sehr groß >15B",
|
||||||
|
"systemAiLegendUnknown": "Größe unbekannt",
|
||||||
|
"systemAiCacheRow": "Cache: {n} {n, plural, =1{gespeicherte Erklärung} other{gespeicherte Erklärungen}}. Identische Prompts treffen den Cache; Modell- oder Privacy-Modus-Wechsel leeren ihn automatisch.",
|
||||||
|
"@systemAiCacheRow": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"systemAiCacheClear": "Leeren",
|
||||||
|
"systemAiCacheClearedToast": "{n} gespeicherte System-AI-Erklärungen gelöscht.",
|
||||||
|
"@systemAiCacheClearedToast": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"systemAiCacheClearFailed": "Cache-Löschen fehlgeschlagen: {error}",
|
||||||
|
"@systemAiCacheClearFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"systemAiPullEmptyError": "Erst eine Modell-ID eintippen (z.B. gemma3:4b).",
|
||||||
|
"systemAiPullFailedError": "Pull fehlgeschlagen: {error}",
|
||||||
|
"@systemAiPullFailedError": { "placeholders": { "error": { "type": "String" } } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,25 @@
|
||||||
"modulesUninstallFailed": "Uninstall failed: {error}",
|
"modulesUninstallFailed": "Uninstall failed: {error}",
|
||||||
"@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } },
|
"@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
"modulesUninstalling": "Uninstalling…",
|
"modulesUninstalling": "Uninstalling…",
|
||||||
|
"moduleSheetFailedToLoad": "Failed to load: {error}",
|
||||||
|
"@moduleSheetFailedToLoad": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"moduleSheetCapabilities": "Capabilities",
|
||||||
|
"moduleSheetPermissions": "Declared permissions",
|
||||||
|
"moduleSheetNoPermissions": "(none — pure-computation module)",
|
||||||
|
|
||||||
|
"flowsTitle": "Flows",
|
||||||
|
"flowsReloadTooltip": "Reload",
|
||||||
|
"flowsNoneTitle": "No saved flows",
|
||||||
|
"flowsNoneHint": "Save a flow with `fai admin flows save <name> <flow.yaml>` and it shows up here.",
|
||||||
|
"flowsRunDialogTitle": "Run {name}",
|
||||||
|
"@flowsRunDialogTitle": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"flowsRunDialogBody": "Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.",
|
||||||
|
"flowsInputsHint": "name=World\ntarget_language=English\nsummary_style=three bullet points",
|
||||||
|
"flowsRunButton": "Run",
|
||||||
|
"flowsRunningTitle": "Running {name}",
|
||||||
|
"@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"flowsRunning": "Flow running…",
|
||||||
|
"flowsNoOutputs": "Flow completed with no declared outputs.",
|
||||||
|
|
||||||
"approvalsTitle": "Approvals",
|
"approvalsTitle": "Approvals",
|
||||||
"approvalsTabPending": "Pending",
|
"approvalsTabPending": "Pending",
|
||||||
|
|
@ -412,5 +431,65 @@
|
||||||
"@searchStoreHintWithTagline": { "placeholders": { "tagline": { "type": "String" } } },
|
"@searchStoreHintWithTagline": { "placeholders": { "tagline": { "type": "String" } } },
|
||||||
"searchStoreHintWithCategory": "store · {category}",
|
"searchStoreHintWithCategory": "store · {category}",
|
||||||
"@searchStoreHintWithCategory": { "placeholders": { "category": { "type": "String" } } },
|
"@searchStoreHintWithCategory": { "placeholders": { "category": { "type": "String" } } },
|
||||||
"searchSavedFlowHint": "saved flow"
|
"searchSavedFlowHint": "saved flow",
|
||||||
|
|
||||||
|
"systemAiTitle": "System AI",
|
||||||
|
"systemAiIntro": "The hub-internal LLM the platform itself uses for inline failure explanations and operator help. Off by default; configure here. See docs/architecture/system-ai.md for what crosses the wire.",
|
||||||
|
"systemAiProviderLabel": "Provider",
|
||||||
|
"systemAiEndpointLabel": "Endpoint URL (up to /v1)",
|
||||||
|
"systemAiApiKeyRequired": "API key env var (required)",
|
||||||
|
"systemAiApiKeyOptional": "API key env var (optional)",
|
||||||
|
"systemAiApiKeyDisclaimer": "Studio never reads or stores the key value — only its env-var name. The hub reads `${name}` at request time.",
|
||||||
|
"@systemAiApiKeyDisclaimer": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
"systemAiTestConnection": "Test connection",
|
||||||
|
"systemAiTesting": "Testing…",
|
||||||
|
"systemAiSave": "Save",
|
||||||
|
"systemAiPrivacyHeader": "PRIVACY MODE",
|
||||||
|
"systemAiPrivacyOffLabel": "Off",
|
||||||
|
"systemAiPrivacyOffDesc": "Feature disabled. No requests leave the hub.",
|
||||||
|
"systemAiPrivacyRedactedLabel": "Redacted",
|
||||||
|
"systemAiPrivacyRedactedDesc": "Only event_type / error / module names. KRITIS-friendly default.",
|
||||||
|
"systemAiPrivacyFullLabel": "Full",
|
||||||
|
"systemAiPrivacyFullDesc": "Includes the audit detail JSON (hash-only — no raw payloads).",
|
||||||
|
"systemAiConnectionOk": "Connection ok",
|
||||||
|
"systemAiConnectionFailed": "Connection failed",
|
||||||
|
"systemAiReplyPrefix": "Reply: {text}",
|
||||||
|
"@systemAiReplyPrefix": { "placeholders": { "text": { "type": "String" } } },
|
||||||
|
"systemAiModelLabel": "Model (required)",
|
||||||
|
"systemAiModelHintFallback": "model identifier the provider expects",
|
||||||
|
"systemAiModelHelperBase": "Required: pick from the list (Refresh) or type one.",
|
||||||
|
"systemAiModelHelperOllama": "Required: pick from the list (Refresh) or type one. Use Pull to download from Ollama.",
|
||||||
|
"systemAiRefresh": "Refresh",
|
||||||
|
"systemAiPull": "Pull",
|
||||||
|
"systemAiPulling": "Pulling…",
|
||||||
|
"systemAiCouldNotListModels": "Could not list models: {error}",
|
||||||
|
"@systemAiCouldNotListModels": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"systemAiNoModelsOllama": "No models on the Ollama server yet. Type one above and click Pull.",
|
||||||
|
"systemAiNoModelsGeneric": "Provider returned no models.",
|
||||||
|
"systemAiSuitabilityRecommended": "recommended",
|
||||||
|
"systemAiSuitabilityBalanced": "balanced",
|
||||||
|
"systemAiSuitabilitySmall": "small — quality may be limited",
|
||||||
|
"systemAiSuitabilityLarge": "large — slower",
|
||||||
|
"systemAiSuitabilityHuge": "huge — likely too slow on a laptop",
|
||||||
|
"systemAiSuitabilityUnknown": "size unknown",
|
||||||
|
"systemAiHwDetected": "Detected: {summary}",
|
||||||
|
"@systemAiHwDetected": { "placeholders": { "summary": { "type": "String" } } },
|
||||||
|
"systemAiHwReviewed": " · curation reviewed {date}",
|
||||||
|
"@systemAiHwReviewed": { "placeholders": { "date": { "type": "String" } } },
|
||||||
|
"systemAiLegendRecommended": "recommended for this hardware{tier}",
|
||||||
|
"@systemAiLegendRecommended": { "placeholders": { "tier": { "type": "String" } } },
|
||||||
|
"systemAiLegendBalanced": "balanced (4-8B)",
|
||||||
|
"systemAiLegendSmallLarge": "small <3B / large 9-15B / above hardware",
|
||||||
|
"systemAiLegendHuge": "huge >15B",
|
||||||
|
"systemAiLegendUnknown": "size unknown",
|
||||||
|
"systemAiCacheRow": "Cache: {n} cached {n, plural, =1{explanation} other{explanations}}. Identical prompts hit the cache; switching model or privacy mode flushes automatically.",
|
||||||
|
"@systemAiCacheRow": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"systemAiCacheClear": "Clear",
|
||||||
|
"systemAiCacheClearedToast": "Cleared {n} cached System-AI explanations.",
|
||||||
|
"@systemAiCacheClearedToast": { "placeholders": { "n": { "type": "int" } } },
|
||||||
|
"systemAiCacheClearFailed": "Cache clear failed: {error}",
|
||||||
|
"@systemAiCacheClearFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||||
|
"systemAiPullEmptyError": "Type a model id (e.g. gemma3:4b) first.",
|
||||||
|
"systemAiPullFailedError": "Pull failed: {error}",
|
||||||
|
"@systemAiPullFailedError": { "placeholders": { "error": { "type": "String" } } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1196,6 +1196,96 @@ abstract class AppLocalizations {
|
||||||
/// **'Uninstalling…'**
|
/// **'Uninstalling…'**
|
||||||
String get modulesUninstalling;
|
String get modulesUninstalling;
|
||||||
|
|
||||||
|
/// No description provided for @moduleSheetFailedToLoad.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to load: {error}'**
|
||||||
|
String moduleSheetFailedToLoad(String error);
|
||||||
|
|
||||||
|
/// No description provided for @moduleSheetCapabilities.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Capabilities'**
|
||||||
|
String get moduleSheetCapabilities;
|
||||||
|
|
||||||
|
/// No description provided for @moduleSheetPermissions.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Declared permissions'**
|
||||||
|
String get moduleSheetPermissions;
|
||||||
|
|
||||||
|
/// No description provided for @moduleSheetNoPermissions.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'(none — pure-computation module)'**
|
||||||
|
String get moduleSheetNoPermissions;
|
||||||
|
|
||||||
|
/// No description provided for @flowsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Flows'**
|
||||||
|
String get flowsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @flowsReloadTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reload'**
|
||||||
|
String get flowsReloadTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @flowsNoneTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No saved flows'**
|
||||||
|
String get flowsNoneTitle;
|
||||||
|
|
||||||
|
/// No description provided for @flowsNoneHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Save a flow with `fai admin flows save <name> <flow.yaml>` and it shows up here.'**
|
||||||
|
String get flowsNoneHint;
|
||||||
|
|
||||||
|
/// No description provided for @flowsRunDialogTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Run {name}'**
|
||||||
|
String flowsRunDialogTitle(String name);
|
||||||
|
|
||||||
|
/// No description provided for @flowsRunDialogBody.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.'**
|
||||||
|
String get flowsRunDialogBody;
|
||||||
|
|
||||||
|
/// No description provided for @flowsInputsHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'name=World\ntarget_language=English\nsummary_style=three bullet points'**
|
||||||
|
String get flowsInputsHint;
|
||||||
|
|
||||||
|
/// No description provided for @flowsRunButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Run'**
|
||||||
|
String get flowsRunButton;
|
||||||
|
|
||||||
|
/// No description provided for @flowsRunningTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Running {name}'**
|
||||||
|
String flowsRunningTitle(String name);
|
||||||
|
|
||||||
|
/// No description provided for @flowsRunning.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Flow running…'**
|
||||||
|
String get flowsRunning;
|
||||||
|
|
||||||
|
/// No description provided for @flowsNoOutputs.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Flow completed with no declared outputs.'**
|
||||||
|
String get flowsNoOutputs;
|
||||||
|
|
||||||
/// No description provided for @approvalsTitle.
|
/// No description provided for @approvalsTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1783,6 +1873,300 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'saved flow'**
|
/// **'saved flow'**
|
||||||
String get searchSavedFlowHint;
|
String get searchSavedFlowHint;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'System AI'**
|
||||||
|
String get systemAiTitle;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The hub-internal LLM the platform itself uses for inline failure explanations and operator help. Off by default; configure here. See docs/architecture/system-ai.md for what crosses the wire.'**
|
||||||
|
String get systemAiIntro;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiProviderLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Provider'**
|
||||||
|
String get systemAiProviderLabel;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiEndpointLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Endpoint URL (up to /v1)'**
|
||||||
|
String get systemAiEndpointLabel;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiApiKeyRequired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'API key env var (required)'**
|
||||||
|
String get systemAiApiKeyRequired;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiApiKeyOptional.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'API key env var (optional)'**
|
||||||
|
String get systemAiApiKeyOptional;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiApiKeyDisclaimer.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Studio never reads or stores the key value — only its env-var name. The hub reads `\${name}` at request time.'**
|
||||||
|
String systemAiApiKeyDisclaimer(String name);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiTestConnection.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Test connection'**
|
||||||
|
String get systemAiTestConnection;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiTesting.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Testing…'**
|
||||||
|
String get systemAiTesting;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiSave.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Save'**
|
||||||
|
String get systemAiSave;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPrivacyHeader.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'PRIVACY MODE'**
|
||||||
|
String get systemAiPrivacyHeader;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPrivacyOffLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Off'**
|
||||||
|
String get systemAiPrivacyOffLabel;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPrivacyOffDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Feature disabled. No requests leave the hub.'**
|
||||||
|
String get systemAiPrivacyOffDesc;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPrivacyRedactedLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Redacted'**
|
||||||
|
String get systemAiPrivacyRedactedLabel;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPrivacyRedactedDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Only event_type / error / module names. KRITIS-friendly default.'**
|
||||||
|
String get systemAiPrivacyRedactedDesc;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPrivacyFullLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Full'**
|
||||||
|
String get systemAiPrivacyFullLabel;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPrivacyFullDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Includes the audit detail JSON (hash-only — no raw payloads).'**
|
||||||
|
String get systemAiPrivacyFullDesc;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiConnectionOk.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connection ok'**
|
||||||
|
String get systemAiConnectionOk;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiConnectionFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connection failed'**
|
||||||
|
String get systemAiConnectionFailed;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiReplyPrefix.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reply: {text}'**
|
||||||
|
String systemAiReplyPrefix(String text);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiModelLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Model (required)'**
|
||||||
|
String get systemAiModelLabel;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiModelHintFallback.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'model identifier the provider expects'**
|
||||||
|
String get systemAiModelHintFallback;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiModelHelperBase.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Required: pick from the list (Refresh) or type one.'**
|
||||||
|
String get systemAiModelHelperBase;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiModelHelperOllama.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Required: pick from the list (Refresh) or type one. Use Pull to download from Ollama.'**
|
||||||
|
String get systemAiModelHelperOllama;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiRefresh.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Refresh'**
|
||||||
|
String get systemAiRefresh;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPull.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pull'**
|
||||||
|
String get systemAiPull;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPulling.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pulling…'**
|
||||||
|
String get systemAiPulling;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiCouldNotListModels.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Could not list models: {error}'**
|
||||||
|
String systemAiCouldNotListModels(String error);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiNoModelsOllama.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No models on the Ollama server yet. Type one above and click Pull.'**
|
||||||
|
String get systemAiNoModelsOllama;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiNoModelsGeneric.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Provider returned no models.'**
|
||||||
|
String get systemAiNoModelsGeneric;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiSuitabilityRecommended.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'recommended'**
|
||||||
|
String get systemAiSuitabilityRecommended;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiSuitabilityBalanced.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'balanced'**
|
||||||
|
String get systemAiSuitabilityBalanced;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiSuitabilitySmall.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'small — quality may be limited'**
|
||||||
|
String get systemAiSuitabilitySmall;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiSuitabilityLarge.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'large — slower'**
|
||||||
|
String get systemAiSuitabilityLarge;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiSuitabilityHuge.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'huge — likely too slow on a laptop'**
|
||||||
|
String get systemAiSuitabilityHuge;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiSuitabilityUnknown.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'size unknown'**
|
||||||
|
String get systemAiSuitabilityUnknown;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiHwDetected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Detected: {summary}'**
|
||||||
|
String systemAiHwDetected(String summary);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiHwReviewed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **' · curation reviewed {date}'**
|
||||||
|
String systemAiHwReviewed(String date);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiLegendRecommended.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'recommended for this hardware{tier}'**
|
||||||
|
String systemAiLegendRecommended(String tier);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiLegendBalanced.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'balanced (4-8B)'**
|
||||||
|
String get systemAiLegendBalanced;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiLegendSmallLarge.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'small <3B / large 9-15B / above hardware'**
|
||||||
|
String get systemAiLegendSmallLarge;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiLegendHuge.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'huge >15B'**
|
||||||
|
String get systemAiLegendHuge;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiLegendUnknown.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'size unknown'**
|
||||||
|
String get systemAiLegendUnknown;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiCacheRow.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cache: {n} cached {n, plural, =1{explanation} other{explanations}}. Identical prompts hit the cache; switching model or privacy mode flushes automatically.'**
|
||||||
|
String systemAiCacheRow(int n);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiCacheClear.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear'**
|
||||||
|
String get systemAiCacheClear;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiCacheClearedToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cleared {n} cached System-AI explanations.'**
|
||||||
|
String systemAiCacheClearedToast(int n);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiCacheClearFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cache clear failed: {error}'**
|
||||||
|
String systemAiCacheClearFailed(String error);
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPullEmptyError.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Type a model id (e.g. gemma3:4b) first.'**
|
||||||
|
String get systemAiPullEmptyError;
|
||||||
|
|
||||||
|
/// No description provided for @systemAiPullFailedError.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pull failed: {error}'**
|
||||||
|
String systemAiPullFailedError(String error);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -663,6 +663,60 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get modulesUninstalling => 'Wird deinstalliert…';
|
String get modulesUninstalling => 'Wird deinstalliert…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String moduleSheetFailedToLoad(String error) {
|
||||||
|
return 'Laden fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moduleSheetCapabilities => 'Capabilities';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moduleSheetPermissions => 'Deklarierte Berechtigungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moduleSheetNoPermissions => '(keine — reines Berechnungsmodul)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsTitle => 'Flows';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsReloadTooltip => 'Aktualisieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsNoneTitle => 'Keine gespeicherten Flows';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsNoneHint =>
|
||||||
|
'Flow speichern mit `fai admin flows save <name> <flow.yaml>`, dann taucht er hier auf.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String flowsRunDialogTitle(String name) {
|
||||||
|
return '$name ausführen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsRunDialogBody =>
|
||||||
|
'Eingaben als key=value, eine pro Zeile. Alle Werte werden als Text-Payloads gesendet. Für binäre Eingaben die `fai run` CLI nutzen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsInputsHint =>
|
||||||
|
'name=World\ntarget_language=English\nsummary_style=three bullet points';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsRunButton => 'Starten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String flowsRunningTitle(String name) {
|
||||||
|
return '$name läuft';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsRunning => 'Flow läuft…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsNoOutputs => 'Flow abgeschlossen ohne deklarierte Outputs.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get approvalsTitle => 'Freigaben';
|
String get approvalsTitle => 'Freigaben';
|
||||||
|
|
||||||
|
|
@ -1005,4 +1059,189 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get searchSavedFlowHint => 'gespeicherter Flow';
|
String get searchSavedFlowHint => 'gespeicherter Flow';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiTitle => 'System-AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiIntro =>
|
||||||
|
'Die hub-interne LLM, die die Plattform selbst für Fehlererklärungen und Operator-Hilfe nutzt. Standardmäßig aus; hier konfigurieren. Siehe docs/architecture/system-ai.md, was über die Leitung geht.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiProviderLabel => 'Provider';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiEndpointLabel => 'Endpunkt-URL (bis /v1)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiApiKeyRequired =>
|
||||||
|
'API-Key-Umgebungsvariable (erforderlich)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiApiKeyOptional => 'API-Key-Umgebungsvariable (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiApiKeyDisclaimer(String name) {
|
||||||
|
return 'Studio liest oder speichert den Key-Wert nie — nur den Namen der Umgebungsvariable. Der Hub liest `\$$name` zur Anfragezeit.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiTestConnection => 'Verbindung testen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiTesting => 'Teste…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSave => 'Speichern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyHeader => 'PRIVACY-MODUS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyOffLabel => 'Aus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyOffDesc =>
|
||||||
|
'Feature deaktiviert. Keine Anfragen verlassen den Hub.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyRedactedLabel => 'Redigiert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyRedactedDesc =>
|
||||||
|
'Nur event_type / error / Modul-Namen. KRITIS-freundlicher Standard.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyFullLabel => 'Voll';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyFullDesc =>
|
||||||
|
'Enthält das Audit-Detail-JSON (nur Hash — keine Roh-Payloads).';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiConnectionOk => 'Verbindung ok';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiConnectionFailed => 'Verbindung fehlgeschlagen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiReplyPrefix(String text) {
|
||||||
|
return 'Antwort: $text';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiModelLabel => 'Modell (erforderlich)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiModelHintFallback =>
|
||||||
|
'Modell-Identifier, den der Provider erwartet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiModelHelperBase =>
|
||||||
|
'Erforderlich: aus der Liste auswählen (Aktualisieren) oder eintippen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiModelHelperOllama =>
|
||||||
|
'Erforderlich: aus der Liste auswählen (Aktualisieren) oder eintippen. Mit Pull von Ollama herunterladen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiRefresh => 'Aktualisieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPull => 'Pull';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPulling => 'Lade…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiCouldNotListModels(String error) {
|
||||||
|
return 'Modelle konnten nicht aufgelistet werden: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiNoModelsOllama =>
|
||||||
|
'Noch keine Modelle auf dem Ollama-Server. Oben eintippen und Pull klicken.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiNoModelsGeneric => 'Provider lieferte keine Modelle.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityRecommended => 'empfohlen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityBalanced => 'ausgewogen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilitySmall => 'klein — Qualität evtl. eingeschränkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityLarge => 'groß — langsamer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityHuge =>
|
||||||
|
'sehr groß — auf einem Laptop wahrscheinlich zu langsam';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityUnknown => 'Größe unbekannt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiHwDetected(String summary) {
|
||||||
|
return 'Erkannt: $summary';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiHwReviewed(String date) {
|
||||||
|
return ' · Kuratierung geprüft $date';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiLegendRecommended(String tier) {
|
||||||
|
return 'empfohlen für diese Hardware$tier';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiLegendBalanced => 'ausgewogen (4-8B)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiLegendSmallLarge =>
|
||||||
|
'klein <3B / groß 9-15B / über Hardware';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiLegendHuge => 'sehr groß >15B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiLegendUnknown => 'Größe unbekannt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiCacheRow(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'gespeicherte Erklärungen',
|
||||||
|
one: 'gespeicherte Erklärung',
|
||||||
|
);
|
||||||
|
return 'Cache: $n $_temp0. Identische Prompts treffen den Cache; Modell- oder Privacy-Modus-Wechsel leeren ihn automatisch.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiCacheClear => 'Leeren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiCacheClearedToast(int n) {
|
||||||
|
return '$n gespeicherte System-AI-Erklärungen gelöscht.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiCacheClearFailed(String error) {
|
||||||
|
return 'Cache-Löschen fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPullEmptyError =>
|
||||||
|
'Erst eine Modell-ID eintippen (z.B. gemma3:4b).';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiPullFailedError(String error) {
|
||||||
|
return 'Pull fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -679,6 +679,60 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get modulesUninstalling => 'Uninstalling…';
|
String get modulesUninstalling => 'Uninstalling…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String moduleSheetFailedToLoad(String error) {
|
||||||
|
return 'Failed to load: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moduleSheetCapabilities => 'Capabilities';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moduleSheetPermissions => 'Declared permissions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get moduleSheetNoPermissions => '(none — pure-computation module)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsTitle => 'Flows';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsReloadTooltip => 'Reload';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsNoneTitle => 'No saved flows';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsNoneHint =>
|
||||||
|
'Save a flow with `fai admin flows save <name> <flow.yaml>` and it shows up here.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String flowsRunDialogTitle(String name) {
|
||||||
|
return 'Run $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsRunDialogBody =>
|
||||||
|
'Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsInputsHint =>
|
||||||
|
'name=World\ntarget_language=English\nsummary_style=three bullet points';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsRunButton => 'Run';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String flowsRunningTitle(String name) {
|
||||||
|
return 'Running $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsRunning => 'Flow running…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get flowsNoOutputs => 'Flow completed with no declared outputs.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get approvalsTitle => 'Approvals';
|
String get approvalsTitle => 'Approvals';
|
||||||
|
|
||||||
|
|
@ -1017,4 +1071,187 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get searchSavedFlowHint => 'saved flow';
|
String get searchSavedFlowHint => 'saved flow';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiTitle => 'System AI';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiIntro =>
|
||||||
|
'The hub-internal LLM the platform itself uses for inline failure explanations and operator help. Off by default; configure here. See docs/architecture/system-ai.md for what crosses the wire.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiProviderLabel => 'Provider';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiEndpointLabel => 'Endpoint URL (up to /v1)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiApiKeyRequired => 'API key env var (required)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiApiKeyOptional => 'API key env var (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiApiKeyDisclaimer(String name) {
|
||||||
|
return 'Studio never reads or stores the key value — only its env-var name. The hub reads `\$$name` at request time.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiTestConnection => 'Test connection';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiTesting => 'Testing…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSave => 'Save';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyHeader => 'PRIVACY MODE';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyOffLabel => 'Off';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyOffDesc =>
|
||||||
|
'Feature disabled. No requests leave the hub.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyRedactedLabel => 'Redacted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyRedactedDesc =>
|
||||||
|
'Only event_type / error / module names. KRITIS-friendly default.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyFullLabel => 'Full';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPrivacyFullDesc =>
|
||||||
|
'Includes the audit detail JSON (hash-only — no raw payloads).';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiConnectionOk => 'Connection ok';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiConnectionFailed => 'Connection failed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiReplyPrefix(String text) {
|
||||||
|
return 'Reply: $text';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiModelLabel => 'Model (required)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiModelHintFallback =>
|
||||||
|
'model identifier the provider expects';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiModelHelperBase =>
|
||||||
|
'Required: pick from the list (Refresh) or type one.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiModelHelperOllama =>
|
||||||
|
'Required: pick from the list (Refresh) or type one. Use Pull to download from Ollama.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiRefresh => 'Refresh';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPull => 'Pull';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPulling => 'Pulling…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiCouldNotListModels(String error) {
|
||||||
|
return 'Could not list models: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiNoModelsOllama =>
|
||||||
|
'No models on the Ollama server yet. Type one above and click Pull.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiNoModelsGeneric => 'Provider returned no models.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityRecommended => 'recommended';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityBalanced => 'balanced';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilitySmall => 'small — quality may be limited';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityLarge => 'large — slower';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityHuge => 'huge — likely too slow on a laptop';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiSuitabilityUnknown => 'size unknown';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiHwDetected(String summary) {
|
||||||
|
return 'Detected: $summary';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiHwReviewed(String date) {
|
||||||
|
return ' · curation reviewed $date';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiLegendRecommended(String tier) {
|
||||||
|
return 'recommended for this hardware$tier';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiLegendBalanced => 'balanced (4-8B)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiLegendSmallLarge =>
|
||||||
|
'small <3B / large 9-15B / above hardware';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiLegendHuge => 'huge >15B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiLegendUnknown => 'size unknown';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiCacheRow(int n) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
n,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'explanations',
|
||||||
|
one: 'explanation',
|
||||||
|
);
|
||||||
|
return 'Cache: $n cached $_temp0. Identical prompts hit the cache; switching model or privacy mode flushes automatically.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiCacheClear => 'Clear';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiCacheClearedToast(int n) {
|
||||||
|
return 'Cleared $n cached System-AI explanations.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiCacheClearFailed(String error) {
|
||||||
|
return 'Cache clear failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAiPullEmptyError =>
|
||||||
|
'Type a model id (e.g. gemma3:4b) first.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String systemAiPullFailedError(String error) {
|
||||||
|
return 'Pull failed: $error';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import 'widgets/widgets.dart';
|
||||||
/// Studio's own build version. Bump on every UI commit so the
|
/// Studio's own build version. Bump on every UI commit so the
|
||||||
/// running app self-identifies — visible in the sidebar header
|
/// running app self-identifies — visible in the sidebar header
|
||||||
/// and quick-glance proof that you're seeing the current build.
|
/// and quick-glance proof that you're seeing the current build.
|
||||||
const String kStudioVersion = '0.27.0';
|
const String kStudioVersion = '0.28.0';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
import '../theme/tokens.dart';
|
import '../theme/tokens.dart';
|
||||||
import '../widgets/widgets.dart';
|
import '../widgets/widgets.dart';
|
||||||
|
|
@ -38,14 +39,15 @@ class _FlowsPageState extends State<FlowsPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Flows'),
|
title: Text(l.flowsTitle),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh, size: 18),
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
tooltip: 'Reload',
|
tooltip: l.flowsReloadTooltip,
|
||||||
onPressed: _refresh,
|
onPressed: _refresh,
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
|
@ -61,21 +63,20 @@ class _FlowsPageState extends State<FlowsPage> {
|
||||||
return FaiEmptyState(
|
return FaiEmptyState(
|
||||||
icon: Icons.cloud_off_outlined,
|
icon: Icons.cloud_off_outlined,
|
||||||
iconColor: Theme.of(context).colorScheme.error,
|
iconColor: Theme.of(context).colorScheme.error,
|
||||||
title: 'Hub unreachable',
|
title: l.hubUnreachable,
|
||||||
hint: 'Start the hub with `fai serve`.',
|
hint: l.hubUnreachableHint,
|
||||||
action: FilledButton.tonal(
|
action: FilledButton.tonal(
|
||||||
onPressed: _refresh,
|
onPressed: _refresh,
|
||||||
child: const Text('Retry'),
|
child: Text(l.buttonRetry),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final flows = snapshot.data ?? [];
|
final flows = snapshot.data ?? [];
|
||||||
if (flows.isEmpty) {
|
if (flows.isEmpty) {
|
||||||
return const FaiEmptyState(
|
return FaiEmptyState(
|
||||||
icon: Icons.account_tree_outlined,
|
icon: Icons.account_tree_outlined,
|
||||||
title: 'No saved flows',
|
title: l.flowsNoneTitle,
|
||||||
hint:
|
hint: l.flowsNoneHint,
|
||||||
'Save a flow with `fai admin flows save <name> <flow.yaml>` and it shows up here.',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
|
|
@ -139,7 +140,7 @@ class _FlowCard extends StatelessWidget {
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: onRun,
|
onPressed: onRun,
|
||||||
icon: const Icon(Icons.play_arrow, size: 16),
|
icon: const Icon(Icons.play_arrow, size: 16),
|
||||||
label: const Text('Run'),
|
label: Text(AppLocalizations.of(context)!.flowsRunButton),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -194,8 +195,9 @@ class _FlowInputDialogState extends State<_FlowInputDialog> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Run ${widget.flow.name}'),
|
title: Text(l.flowsRunDialogTitle(widget.flow.name)),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||||
),
|
),
|
||||||
|
|
@ -206,7 +208,7 @@ class _FlowInputDialogState extends State<_FlowInputDialog> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.',
|
l.flowsRunDialogBody,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -219,10 +221,9 @@ class _FlowInputDialogState extends State<_FlowInputDialog> {
|
||||||
expands: true,
|
expands: true,
|
||||||
textAlignVertical: TextAlignVertical.top,
|
textAlignVertical: TextAlignVertical.top,
|
||||||
style: FaiTheme.mono(size: 12),
|
style: FaiTheme.mono(size: 12),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText:
|
hintText: l.flowsInputsHint,
|
||||||
'name=World\ntarget_language=English\nsummary_style=three bullet points',
|
border: const OutlineInputBorder(),
|
||||||
border: OutlineInputBorder(),
|
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -233,11 +234,11 @@ class _FlowInputDialogState extends State<_FlowInputDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context, null),
|
onPressed: () => Navigator.pop(context, null),
|
||||||
child: const Text('Cancel'),
|
child: Text(l.buttonCancel),
|
||||||
),
|
),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
icon: const Icon(Icons.play_arrow, size: 16),
|
icon: const Icon(Icons.play_arrow, size: 16),
|
||||||
label: const Text('Run'),
|
label: Text(l.flowsRunButton),
|
||||||
onPressed: () => Navigator.pop(context, _parsePairs()),
|
onPressed: () => Navigator.pop(context, _parsePairs()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -270,8 +271,9 @@ class _FlowRunDialogState extends State<_FlowRunDialog> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Running ${widget.flow.name}'),
|
title: Text(l.flowsRunningTitle(widget.flow.name)),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||||
),
|
),
|
||||||
|
|
@ -288,7 +290,7 @@ class _FlowRunDialogState extends State<_FlowRunDialog> {
|
||||||
const CircularProgressIndicator(),
|
const CircularProgressIndicator(),
|
||||||
const SizedBox(height: FaiSpace.md),
|
const SizedBox(height: FaiSpace.md),
|
||||||
Text(
|
Text(
|
||||||
'Flow running…',
|
l.flowsRunning,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -322,7 +324,7 @@ class _FlowRunDialogState extends State<_FlowRunDialog> {
|
||||||
final outputs = snapshot.data ?? const {};
|
final outputs = snapshot.data ?? const {};
|
||||||
if (outputs.isEmpty) {
|
if (outputs.isEmpty) {
|
||||||
return Text(
|
return Text(
|
||||||
'Flow completed with no declared outputs.',
|
l.flowsNoOutputs,
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -368,7 +370,7 @@ class _FlowRunDialogState extends State<_FlowRunDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Close'),
|
child: Text(l.buttonClose),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,8 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(FaiSpace.xl),
|
padding: const EdgeInsets.all(FaiSpace.xl),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Failed to load: ${snapshot.error}',
|
AppLocalizations.of(context)!
|
||||||
|
.moduleSheetFailedToLoad(snapshot.error.toString()),
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
|
|
@ -168,6 +169,7 @@ class _Body extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
FaiSpace.xxl,
|
FaiSpace.xxl,
|
||||||
|
|
@ -202,7 +204,7 @@ class _Body extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.xl),
|
const SizedBox(height: FaiSpace.xl),
|
||||||
_SectionHeader('Capabilities'),
|
_SectionHeader(l.moduleSheetCapabilities),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: FaiSpace.xs,
|
spacing: FaiSpace.xs,
|
||||||
|
|
@ -218,11 +220,11 @@ class _Body extends StatelessWidget {
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.xl),
|
const SizedBox(height: FaiSpace.xl),
|
||||||
_SectionHeader('Declared permissions'),
|
_SectionHeader(l.moduleSheetPermissions),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
if (detail.permissions.isEmpty)
|
if (detail.permissions.isEmpty)
|
||||||
Text(
|
Text(
|
||||||
'(none — pure-computation module)',
|
l.moduleSheetNoPermissions,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
import '../theme/tokens.dart';
|
import '../theme/tokens.dart';
|
||||||
import 'fai_pill.dart';
|
import 'fai_pill.dart';
|
||||||
|
|
@ -244,19 +245,18 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
try {
|
try {
|
||||||
final purged = await HubService.instance.clearSystemLlmCache();
|
final purged = await HubService.instance.clearSystemLlmCache();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Cleared $purged cached System-AI explanations.')),
|
SnackBar(content: Text(l.systemAiCacheClearedToast(purged))),
|
||||||
);
|
);
|
||||||
// Pop with the same status (since the editor's `initial`
|
|
||||||
// is now stale w.r.t. cacheCount); caller will re-fetch
|
|
||||||
// status on next open.
|
|
||||||
setState(() {
|
setState(() {
|
||||||
// Force a rebuild that hides the row; cheap.
|
// Force a rebuild that hides the row; cheap.
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Cache clear failed: $e')),
|
SnackBar(content: Text(l.systemAiCacheClearFailed(e.toString()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,8 +315,9 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
|
|
||||||
Future<void> _pullModel() async {
|
Future<void> _pullModel() async {
|
||||||
final wanted = _model.text.trim();
|
final wanted = _model.text.trim();
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
if (wanted.isEmpty) {
|
if (wanted.isEmpty) {
|
||||||
setState(() => _error = 'Type a model id (e.g. gemma3:4b) first.');
|
setState(() => _error = l.systemAiPullEmptyError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -332,7 +333,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pulling = false;
|
_pulling = false;
|
||||||
if (r.errorKind.isNotEmpty) {
|
if (r.errorKind.isNotEmpty) {
|
||||||
_error = 'Pull failed: ${r.text}';
|
_error = l.systemAiPullFailedError(r.text);
|
||||||
} else {
|
} else {
|
||||||
_error = null;
|
_error = null;
|
||||||
}
|
}
|
||||||
|
|
@ -347,8 +348,9 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('System AI'),
|
title: Text(l.systemAiTitle),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||||
),
|
),
|
||||||
|
|
@ -364,9 +366,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'The hub-internal LLM the platform itself uses for inline failure '
|
l.systemAiIntro,
|
||||||
'explanations and operator help. Off by default; configure here. '
|
|
||||||
'See docs/architecture/system-ai.md for what crosses the wire.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -391,9 +391,9 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
TextField(
|
TextField(
|
||||||
controller: _endpoint,
|
controller: _endpoint,
|
||||||
style: FaiTheme.mono(size: 12),
|
style: FaiTheme.mono(size: 12),
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Endpoint URL (up to /v1)',
|
labelText: l.systemAiEndpointLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -420,8 +420,8 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
style: FaiTheme.mono(size: 12),
|
style: FaiTheme.mono(size: 12),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: _preset.wire == 'openai'
|
labelText: _preset.wire == 'openai'
|
||||||
? 'API key env var (required)'
|
? l.systemAiApiKeyRequired
|
||||||
: 'API key env var (optional)',
|
: l.systemAiApiKeyOptional,
|
||||||
hintText: 'OPENAI_API_KEY',
|
hintText: 'OPENAI_API_KEY',
|
||||||
prefixText: '\$',
|
prefixText: '\$',
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
|
@ -430,8 +430,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Studio never reads or stores the key value — only its env-var name. '
|
l.systemAiApiKeyDisclaimer('<name>'),
|
||||||
'The hub reads `\$<name>` at request time.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -470,12 +469,12 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
onPressed: _saving || _testing
|
onPressed: _saving || _testing
|
||||||
? null
|
? null
|
||||||
: () => Navigator.pop(context, null),
|
: () => Navigator.pop(context, null),
|
||||||
child: const Text('Cancel'),
|
child: Text(l.buttonCancel),
|
||||||
),
|
),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _saving || _testing ? null : _test,
|
onPressed: _saving || _testing ? null : _test,
|
||||||
icon: const Icon(Icons.play_arrow, size: 16),
|
icon: const Icon(Icons.play_arrow, size: 16),
|
||||||
label: Text(_testing ? 'Testing…' : 'Test connection'),
|
label: Text(_testing ? l.systemAiTesting : l.systemAiTestConnection),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _saving || _testing ? null : () => _save(),
|
onPressed: _saving || _testing ? null : () => _save(),
|
||||||
|
|
@ -485,7 +484,7 @@ class _FaiSystemAiEditorState extends State<FaiSystemAiEditor> {
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Text('Save'),
|
: Text(l.systemAiSave),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -503,12 +502,13 @@ class _ProviderDropdown extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
initialValue: value.wire,
|
initialValue: value.wire,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Provider',
|
labelText: l.systemAiProviderLabel,
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
items: _ProviderPreset.all
|
items: _ProviderPreset.all
|
||||||
|
|
@ -539,24 +539,25 @@ class _PrivacyModeChips extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
const modes = <(String, String, String)>[
|
final l = AppLocalizations.of(context)!;
|
||||||
('off', 'Off', 'Feature disabled. No requests leave the hub.'),
|
final modes = <(String, String, String)>[
|
||||||
|
('off', l.systemAiPrivacyOffLabel, l.systemAiPrivacyOffDesc),
|
||||||
(
|
(
|
||||||
'redacted',
|
'redacted',
|
||||||
'Redacted',
|
l.systemAiPrivacyRedactedLabel,
|
||||||
'Only event_type / error / module names. KRITIS-friendly default.',
|
l.systemAiPrivacyRedactedDesc,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'full',
|
'full',
|
||||||
'Full',
|
l.systemAiPrivacyFullLabel,
|
||||||
'Includes the audit detail JSON (hash-only — no raw payloads).',
|
l.systemAiPrivacyFullDesc,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'PRIVACY MODE',
|
l.systemAiPrivacyHeader,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
letterSpacing: 0.6,
|
letterSpacing: 0.6,
|
||||||
|
|
@ -611,6 +612,7 @@ class _TestResultPanel extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final ok = result.isSuccess;
|
final ok = result.isSuccess;
|
||||||
final color = ok ? FaiColors.success : theme.colorScheme.error;
|
final color = ok ? FaiColors.success : theme.colorScheme.error;
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -633,7 +635,7 @@ class _TestResultPanel extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.xs),
|
const SizedBox(width: FaiSpace.xs),
|
||||||
Text(
|
Text(
|
||||||
ok ? 'Connection ok' : 'Connection failed',
|
ok ? l.systemAiConnectionOk : l.systemAiConnectionFailed,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
|
|
@ -649,7 +651,7 @@ class _TestResultPanel extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
ok ? 'Reply: ${result.text}' : result.text,
|
ok ? l.systemAiReplyPrefix(result.text) : result.text,
|
||||||
style: FaiTheme.mono(
|
style: FaiTheme.mono(
|
||||||
size: 11,
|
size: 11,
|
||||||
color: theme.colorScheme.onSurface,
|
color: theme.colorScheme.onSurface,
|
||||||
|
|
@ -707,6 +709,7 @@ class _ModelPicker extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -720,12 +723,14 @@ class _ModelPicker extends StatelessWidget {
|
||||||
controller: controller,
|
controller: controller,
|
||||||
style: FaiTheme.mono(size: 12),
|
style: FaiTheme.mono(size: 12),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Model (required)',
|
labelText: l.systemAiModelLabel,
|
||||||
hintText: preset.modelHint.isEmpty
|
hintText: preset.modelHint.isEmpty
|
||||||
? 'model identifier the provider expects'
|
? l.systemAiModelHintFallback
|
||||||
: preset.modelHint,
|
: preset.modelHint,
|
||||||
helperText: controller.text.trim().isEmpty
|
helperText: controller.text.trim().isEmpty
|
||||||
? 'Required: pick from the list (Refresh) or type one. ${_isOllama ? "Use Pull to download from Ollama." : ""}'
|
? (_isOllama
|
||||||
|
? l.systemAiModelHelperOllama
|
||||||
|
: l.systemAiModelHelperBase)
|
||||||
: null,
|
: null,
|
||||||
helperMaxLines: 2,
|
helperMaxLines: 2,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
|
@ -744,7 +749,7 @@ class _ModelPicker extends StatelessWidget {
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.refresh, size: 16),
|
: const Icon(Icons.refresh, size: 16),
|
||||||
label: const Text('Refresh'),
|
label: Text(l.systemAiRefresh),
|
||||||
),
|
),
|
||||||
if (_isOllama) ...[
|
if (_isOllama) ...[
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
|
|
@ -757,7 +762,7 @@ class _ModelPicker extends StatelessWidget {
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.download, size: 16),
|
: const Icon(Icons.download, size: 16),
|
||||||
label: Text(pulling ? 'Pulling…' : 'Pull'),
|
label: Text(pulling ? l.systemAiPulling : l.systemAiPull),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -765,7 +770,7 @@ class _ModelPicker extends StatelessWidget {
|
||||||
if (modelsError != null) ...[
|
if (modelsError != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Could not list models: $modelsError',
|
l.systemAiCouldNotListModels(modelsError!),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
|
|
@ -776,8 +781,8 @@ class _ModelPicker extends StatelessWidget {
|
||||||
if (models!.isEmpty)
|
if (models!.isEmpty)
|
||||||
Text(
|
Text(
|
||||||
_isOllama
|
_isOllama
|
||||||
? 'No models on the Ollama server yet. Type one above and click Pull.'
|
? l.systemAiNoModelsOllama
|
||||||
: 'Provider returned no models.',
|
: l.systemAiNoModelsGeneric,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -827,20 +832,21 @@ enum _Suitability {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _SuitabilityCopy on _Suitability {
|
extension _SuitabilityCopy on _Suitability {
|
||||||
String get label {
|
String labelFor(BuildContext context) {
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case _Suitability.recommended:
|
case _Suitability.recommended:
|
||||||
return 'recommended';
|
return l.systemAiSuitabilityRecommended;
|
||||||
case _Suitability.balanced:
|
case _Suitability.balanced:
|
||||||
return 'balanced';
|
return l.systemAiSuitabilityBalanced;
|
||||||
case _Suitability.small:
|
case _Suitability.small:
|
||||||
return 'small — quality may be limited';
|
return l.systemAiSuitabilitySmall;
|
||||||
case _Suitability.large:
|
case _Suitability.large:
|
||||||
return 'large — slower';
|
return l.systemAiSuitabilityLarge;
|
||||||
case _Suitability.huge:
|
case _Suitability.huge:
|
||||||
return 'huge — likely too slow on a laptop';
|
return l.systemAiSuitabilityHuge;
|
||||||
case _Suitability.unknown:
|
case _Suitability.unknown:
|
||||||
return 'size unknown';
|
return l.systemAiSuitabilityUnknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -970,8 +976,8 @@ class _ModelChip extends StatelessWidget {
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
String _tooltipFor(BuildContext _) {
|
String _tooltipFor(BuildContext context) {
|
||||||
final lines = <String>[suitability.label];
|
final lines = <String>[suitability.labelFor(context)];
|
||||||
final c = curated;
|
final c = curated;
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
lines.add(
|
lines.add(
|
||||||
|
|
@ -1023,8 +1029,9 @@ class _HardwareBanner extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
final lr = (lastReviewed != null && lastReviewed!.isNotEmpty)
|
final lr = (lastReviewed != null && lastReviewed!.isNotEmpty)
|
||||||
? ' · curation reviewed $lastReviewed'
|
? l.systemAiHwReviewed(lastReviewed!)
|
||||||
: '';
|
: '';
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
|
@ -1046,7 +1053,7 @@ class _HardwareBanner extends StatelessWidget {
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Detected: ${hw.summary}$lr',
|
'${l.systemAiHwDetected(hw.summary)}$lr',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -1065,6 +1072,7 @@ class _SuitabilityLegend extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
Widget dot(Color c, String label) => Padding(
|
Widget dot(Color c, String label) => Padding(
|
||||||
padding: const EdgeInsets.only(right: FaiSpace.md),
|
padding: const EdgeInsets.only(right: FaiSpace.md),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -1094,7 +1102,7 @@ class _SuitabilityLegend extends StatelessWidget {
|
||||||
Icon(Icons.star, size: 11, color: FaiColors.success),
|
Icon(Icons.star, size: 11, color: FaiColors.success),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'recommended for this hardware$tierTag',
|
l.systemAiLegendRecommended(tierTag),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
|
@ -1103,10 +1111,10 @@ class _SuitabilityLegend extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dot(theme.colorScheme.primary, 'balanced (4-8B)'),
|
dot(theme.colorScheme.primary, l.systemAiLegendBalanced),
|
||||||
dot(FaiColors.warning, 'small <3B / large 9-15B / above hardware'),
|
dot(FaiColors.warning, l.systemAiLegendSmallLarge),
|
||||||
dot(theme.colorScheme.error, 'huge >15B'),
|
dot(theme.colorScheme.error, l.systemAiLegendHuge),
|
||||||
dot(theme.colorScheme.outline, 'size unknown'),
|
dot(theme.colorScheme.outline, l.systemAiLegendUnknown),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1126,6 +1134,7 @@ class _CacheStatusRow extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final l = AppLocalizations.of(context)!;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: FaiSpace.sm,
|
horizontal: FaiSpace.sm,
|
||||||
|
|
@ -1146,9 +1155,7 @@ class _CacheStatusRow extends StatelessWidget {
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Cache: $cacheCount cached explanation${cacheCount == 1 ? "" : "s"}. '
|
l.systemAiCacheRow(cacheCount),
|
||||||
'Identical prompts hit the cache; switching model or privacy '
|
|
||||||
'mode flushes automatically.',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -1157,7 +1164,7 @@ class _CacheStatusRow extends StatelessWidget {
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onClear,
|
onPressed: onClear,
|
||||||
icon: const Icon(Icons.delete_outline, size: 14),
|
icon: const Icon(Icons.delete_outline, size: 14),
|
||||||
label: const Text('Clear'),
|
label: Text(l.systemAiCacheClear),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.27.0
|
version: 0.28.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue