| #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<string, string[]> s_Cache = new Dictionary<string, string[]>(); |
| static readonly Dictionary<string, double> s_CacheAt = new Dictionary<string, double>(); |
| 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<string>(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<string> 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<string>(); |
| try |
| { |
| var url = endpoint.TrimEnd('/') + "/api/tags"; |
| var json = s_Http.GetStringAsync(url).GetAwaiter().GetResult(); |
| var parsed = JsonUtility.FromJson<TagsResponse>(json); |
| if (parsed != null && parsed.models != null) |
| { |
| var names = new List<string>(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 |
| { |
| |
| } |
|
|
| 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 |
|
|