com.sky.ondeviceagent / Runtime /AgentCore /Editor /OllamaModelDropdownDrawer.cs
Sky-Kim's picture
Initial commit
2e7837a
Raw
History Blame Contribute Delete
5.47 kB
#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
{
// 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