#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Net.Http; using UnityEditor; using UnityEngine; using OnDeviceAgent.AgentCore; namespace OnDeviceAgent.AgentCore.Editor { [CustomPropertyDrawer(typeof(OllamaModelDropdownAttribute))] public sealed class OllamaModelDropdownDrawer : PropertyDrawer { static readonly Dictionary s_Cache = new Dictionary(); static readonly Dictionary s_CacheAt = new Dictionary(); static readonly HttpClient s_Http = new HttpClient { Timeout = TimeSpan.FromSeconds(1.5) }; const double CacheSeconds = 30.0; public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { if (property.propertyType != SerializedPropertyType.String) { EditorGUI.PropertyField(position, property, label); return; } var attr = (OllamaModelDropdownAttribute)attribute; var endpoint = ResolveEndpoint(attr, property); var current = property.stringValue ?? string.Empty; var models = GetModels(endpoint); var options = new List(models.Length + 1); if (!string.IsNullOrEmpty(current) && Array.IndexOf(models, current) < 0) options.Add(current + " (not in /api/tags)"); options.AddRange(models); if (options.Count == 0) options.Add(string.IsNullOrEmpty(current) ? "(no models)" : current); var selectedIndex = 0; for (var i = 0; i < options.Count; i++) { var name = options[i]; var marker = name.IndexOf(" (not in", StringComparison.Ordinal); var bare = marker >= 0 ? name.Substring(0, marker) : name; if (bare == current) { selectedIndex = i; break; } } const float refreshWidth = 22f; var popupRect = new Rect(position.x, position.y, position.width - refreshWidth - 2f, position.height); var refreshRect = new Rect(position.xMax - refreshWidth, position.y, refreshWidth, position.height); EditorGUI.BeginChangeCheck(); var newIndex = EditorGUI.Popup(popupRect, label, selectedIndex, ToGuiContents(options)); if (EditorGUI.EndChangeCheck()) { var chosen = options[newIndex]; var marker = chosen.IndexOf(" (not in", StringComparison.Ordinal); property.stringValue = marker >= 0 ? chosen.Substring(0, marker) : chosen; } if (GUI.Button(refreshRect, new GUIContent("↻", "Refresh models from " + endpoint))) { s_CacheAt[endpoint] = 0; GetModels(endpoint); EditorWindow.focusedWindow?.Repaint(); } } static GUIContent[] ToGuiContents(List items) { var arr = new GUIContent[items.Count]; for (var i = 0; i < items.Count; i++) arr[i] = new GUIContent(items[i]); return arr; } static string ResolveEndpoint(OllamaModelDropdownAttribute attr, SerializedProperty property) { if (!string.IsNullOrWhiteSpace(attr.EndpointField)) { var sibling = property.serializedObject.FindProperty(attr.EndpointField); if (sibling != null && sibling.propertyType == SerializedPropertyType.String && !string.IsNullOrWhiteSpace(sibling.stringValue)) return sibling.stringValue.Trim(); } return string.IsNullOrWhiteSpace(attr.Endpoint) ? "http://localhost:11434" : attr.Endpoint.Trim(); } static string[] GetModels(string endpoint) { if (s_Cache.TryGetValue(endpoint, out var cached) && s_CacheAt.TryGetValue(endpoint, out var at) && EditorApplication.timeSinceStartup - at < CacheSeconds) return cached; var result = Array.Empty(); try { var url = endpoint.TrimEnd('/') + "/api/tags"; var json = s_Http.GetStringAsync(url).GetAwaiter().GetResult(); var parsed = JsonUtility.FromJson(json); if (parsed != null && parsed.models != null) { var names = new List(parsed.models.Length); for (var i = 0; i < parsed.models.Length; i++) { var n = parsed.models[i] != null ? parsed.models[i].name : null; if (!string.IsNullOrEmpty(n)) names.Add(n); } names.Sort(StringComparer.Ordinal); result = names.ToArray(); } } catch { // Endpoint unreachable, non-fatal; current value is preserved. } s_Cache[endpoint] = result; s_CacheAt[endpoint] = EditorApplication.timeSinceStartup; return result; } [Serializable] sealed class TagsResponse { public TagEntry[] models; } [Serializable] sealed class TagEntry { public string name; } } } #endif