using System; using System.Collections.Generic; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Security; using MCPForUnity.Editor.Services.AssetGen.Import; using UnityEngine; using UnityEngine.UIElements; namespace MCPForUnity.Editor.Windows.Components.AssetGen { /// /// Controller for the AI Asset Generation settings tab. This tab is CONFIG ONLY: /// it lets users enter/clear per-provider API keys, toggle providers on/off, /// presence-check a key, and set non-secret generation preferences. /// Generation itself is never triggered here — only via MCP tools / CLI. /// /// Keys are written to the OS secure store (), never to /// EditorPrefs or the project. The stored key is never read back into the field; only /// its presence is surfaced through the status label. /// public class McpAssetGenSection { // Fixed provider lists. Each Id is both the SecureKeyStore key and the // AssetGenPrefs enable-flag id. All model/marketplace providers below emit GLB. private static readonly (string Id, string Label)[] ModelProviders = { ("tripo", "Tripo"), ("meshy", "Meshy"), ("sketchfab", "Sketchfab"), }; private static readonly (string Id, string Label)[] ImageProviders = { ("fal", "fal"), ("openrouter", "OpenRouter"), }; // UI Elements private VisualElement providersContainer; private VisualElement gltfastNotice; private DropdownField formatDropdown; private TextField outputRootField; private Toggle autoNormalizeToggle; // Per-provider enable toggles for the GLB-capable (model) providers, used to // recompute the glTFast notice when a toggle changes. private readonly List<(string Id, Toggle Toggle)> modelEnableToggles = new(); public VisualElement Root { get; private set; } public McpAssetGenSection(VisualElement root) { Root = root; CacheUIElements(); InitializeUI(); RegisterCallbacks(); } private void CacheUIElements() { providersContainer = Root.Q("assetgen-providers-container"); gltfastNotice = Root.Q("gltfast-notice"); formatDropdown = Root.Q("assetgen-format-dropdown"); outputRootField = Root.Q("assetgen-output-root"); autoNormalizeToggle = Root.Q("assetgen-auto-normalize"); } private void InitializeUI() { // One-time choices + tooltips; the field values are populated by SyncFromPrefs. if (formatDropdown != null) { formatDropdown.choices = new List { "glb", "fbx", "obj" }; formatDropdown.tooltip = "Default container format for generated 3D models."; } if (outputRootField != null) { outputRootField.tooltip = $"Project-relative folder where generated assets are written. Empty = {AssetGenPrefs.DefaultOutputRoot}."; } if (autoNormalizeToggle != null) { autoNormalizeToggle.tooltip = "Uniformly scale imported models to the target size on import."; } SyncFromPrefs(); } private void RegisterCallbacks() { if (formatDropdown != null) { formatDropdown.RegisterValueChangedCallback(evt => { AssetGenPrefs.DefaultFormat = evt.newValue; }); } if (outputRootField != null) { outputRootField.RegisterCallback(_ => { AssetGenPrefs.OutputRoot = outputRootField.text?.Trim(); // Reflect the normalized/default value (empty -> default) without re-triggering. outputRootField.SetValueWithoutNotify(AssetGenPrefs.OutputRoot); }); } if (autoNormalizeToggle != null) { autoNormalizeToggle.RegisterValueChangedCallback(evt => { AssetGenPrefs.AutoNormalize = evt.newValue; }); } } /// /// Re-reads secure-store presence and prefs and rebuilds the rows. Called when the /// tab becomes visible so keys set elsewhere (e.g. via CLI) are reflected. /// public void Refresh() => SyncFromPrefs(); /// Rebuild the provider rows and reflect current prefs into the fields. private void SyncFromPrefs() { BuildProviderRows(); formatDropdown?.SetValueWithoutNotify(NormalizeFormat(AssetGenPrefs.DefaultFormat)); outputRootField?.SetValueWithoutNotify(AssetGenPrefs.OutputRoot); autoNormalizeToggle?.SetValueWithoutNotify(AssetGenPrefs.AutoNormalize); UpdateGltfastNotice(); } private void BuildProviderRows() { if (providersContainer == null) { return; } providersContainer.Clear(); modelEnableToggles.Clear(); AddGroupLabel("3D Model Providers"); foreach (var provider in ModelProviders) { var toggle = AddProviderRow(provider.Id, provider.Label); modelEnableToggles.Add((provider.Id, toggle)); } AddGroupLabel("Image Providers"); foreach (var provider in ImageProviders) { AddProviderRow(provider.Id, provider.Label); } AddBlenderHandoffRow(); } private void AddGroupLabel(string text) { var label = new Label(text); label.AddToClassList("config-label"); providersContainer.Add(label); } /// /// Informational handoff row (not a keyed provider): best-effort "is Blender installed" /// status + a pointer to the blender-to-unity workflow. BlenderMCP itself runs in the AI /// client and isn't detectable from Unity, so this only reports the local Blender app. /// private void AddBlenderHandoffRow() { AddGroupLabel("Blender → Unity Handoff"); var row = new VisualElement(); row.style.marginBottom = 8; bool blender = BlenderDetection.IsInstalled(); var status = new Label(blender ? "Blender app detected ✓" : "Blender app not found on this machine"); status.AddToClassList("help-text"); status.style.color = blender ? new Color(0.4f, 0.8f, 0.4f) : new Color(0.7f, 0.7f, 0.7f); row.Add(status); var help = new Label( "Pair Blender with the BlenderMCP server in your AI client, then run the blender-to-unity " + "skill to export the current model — it imports via the import_model_file tool. (BlenderMCP " + "is configured in your AI client and can't be detected here.)"); help.AddToClassList("help-text"); help.style.whiteSpace = WhiteSpace.Normal; row.Add(help); providersContainer.Add(row); } private Toggle AddProviderRow(string id, string displayName) { var row = new VisualElement(); row.style.marginBottom = 8; row.style.paddingBottom = 8; row.style.borderBottomWidth = 1; row.style.borderBottomColor = new Color(0.3f, 0.3f, 0.3f, 0.3f); // Header: bold provider name + enable toggle. var header = new VisualElement(); header.style.flexDirection = FlexDirection.Row; header.style.alignItems = Align.Center; header.style.marginBottom = 2; var nameLabel = new Label(displayName); nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold; nameLabel.style.flexGrow = 1; header.Add(nameLabel); var enableToggle = new Toggle("Enabled"); enableToggle.SetValueWithoutNotify(AssetGenPrefs.IsProviderEnabled(id)); enableToggle.tooltip = $"Enable the {displayName} provider for asset generation."; header.Add(enableToggle); row.Add(header); var statusLabel = new Label(); statusLabel.AddToClassList("help-text"); // Masked key field + Save / Clear / Test buttons. var fieldRow = new VisualElement(); fieldRow.style.flexDirection = FlexDirection.Row; fieldRow.style.alignItems = Align.Center; var keyField = new TextField(); keyField.isPasswordField = true; keyField.maskChar = '*'; keyField.style.flexGrow = 1; keyField.style.flexShrink = 1; keyField.style.marginRight = 4; keyField.tooltip = $"Paste your {displayName} API key, then press Save (or click away). " + "The key is stored in your OS secure store and is never read back into this field."; fieldRow.Add(keyField); var saveButton = new Button { text = "Save" }; saveButton.AddToClassList("icon-button"); fieldRow.Add(saveButton); var clearButton = new Button { text = "Clear" }; clearButton.AddToClassList("icon-button"); fieldRow.Add(clearButton); var testButton = new Button { text = "Test" }; testButton.AddToClassList("icon-button"); fieldRow.Add(testButton); row.Add(fieldRow); row.Add(statusLabel); // Persist the typed key, then clear the field so the secret is never displayed. void SaveKeyFromField() { string text = keyField.text?.Trim(); if (string.IsNullOrEmpty(text)) { return; } try { SecureKeyStore.Current.Set(id, text); keyField.SetValueWithoutNotify(string.Empty); SetStatus(statusLabel, "saved ✓", true); } catch (Exception ex) { McpLog.Warn($"Failed to store {id} key: {ex.Message}"); SetStatus(statusLabel, "save failed", false); } } keyField.RegisterCallback(_ => SaveKeyFromField()); saveButton.clicked += SaveKeyFromField; clearButton.clicked += () => { try { SecureKeyStore.Current.Delete(id); } catch (Exception ex) { McpLog.Warn($"Failed to delete {id} key: {ex.Message}"); } keyField.SetValueWithoutNotify(string.Empty); SetStatus(statusLabel, "not set", false); }; // v1 surfaces presence only. Live endpoint validation (an actual auth ping to the // provider) is a future enhancement and intentionally not performed here. testButton.clicked += () => { bool present = HasKey(id); SetStatus(statusLabel, present ? "key present ✓" : "no key set", present); }; enableToggle.RegisterValueChangedCallback(evt => { AssetGenPrefs.SetProviderEnabled(id, evt.newValue); UpdateGltfastNotice(); }); // Initial status reflects secure-store presence (existence only; never the value). bool has = HasKey(id); SetStatus(statusLabel, has ? "saved ✓" : "not set", has); providersContainer.Add(row); return enableToggle; } private static void SetStatus(Label label, string text, bool ok) { if (label == null) { return; } label.text = text; label.style.color = ok ? new Color(0.4f, 0.8f, 0.4f) : new Color(0.7f, 0.7f, 0.7f); } private static bool HasKey(string id) { try { return SecureKeyStore.Current.Has(id); } catch { return false; } } private static string NormalizeFormat(string format) { switch ((format ?? string.Empty).Trim().ToLowerInvariant()) { case "glb": case "fbx": case "obj": return format.Trim().ToLowerInvariant(); default: return AssetGenPrefs.DefaultFormatValue; } } private void UpdateGltfastNotice() { if (gltfastNotice == null) { return; } bool anyGlbProviderEnabled = false; foreach (var entry in modelEnableToggles) { bool enabled = entry.Toggle != null ? entry.Toggle.value : AssetGenPrefs.IsProviderEnabled(entry.Id); if (enabled) { anyGlbProviderEnabled = true; break; } } bool show = anyGlbProviderEnabled && !ModelImportPipeline.IsGltfastAvailable(); gltfastNotice.EnableInClassList("visible", show); } } }