You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kev/Drawer/AI/Models/SpecialMessages/ParameterSetMessage.cs

383 lines
15 KiB
C#

1 month ago
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using YamlDotNet.Serialization;
namespace AI.Models.SpecialMessages
{
/// <summary>
/// 参数集单条字段的控件类型(用于可编辑参数卡片)
/// </summary>
public enum ParameterSetFieldType
{
/// <summary>单行文本</summary>
Text,
/// <summary>数值</summary>
Number,
/// <summary>下拉选择(如成图算法)</summary>
Choice,
/// <summary>文件路径,带“选择文件”按钮</summary>
FilePath,
}
/// <summary>
/// 单条参数项:名称、当前值(可编辑)、描述、控件类型及选项。
/// </summary>
public class ParameterSetItem : INotifyPropertyChanged
{
private string _name = string.Empty;
private string _valueText = string.Empty;
private string _description = string.Empty;
private ParameterSetFieldType _fieldType = ParameterSetFieldType.Text;
public string Name
{
get => _name;
set => SetProperty(ref _name, value ?? string.Empty);
}
/// <summary>当前值的展示/编辑文本</summary>
public string ValueText
{
get => _valueText;
set
{
if (SetProperty(ref _valueText, value ?? string.Empty))
{
OnPropertyChanged(nameof(NumericValue));
}
}
}
/// <summary>
/// 数字值(仅 Number 类型控件绑定使用)。
/// 写回时仅在 FieldType == Number 时才更新 ValueText
/// 防止不可见 NumericUpDown 的双向绑定覆盖 FilePath / Text 等字段的值。
/// </summary>
public double NumericValue
{
get => double.TryParse(
_valueText?.Trim(),
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out var n) ? n : 0d;
set
{
if (_fieldType == ParameterSetFieldType.Number)
{
var s = value.ToString(System.Globalization.CultureInfo.InvariantCulture);
if (SetProperty(ref _valueText, s, nameof(ValueText)))
{
OnPropertyChanged(nameof(NumericValue));
}
}
}
}
public string Description
{
get => _description;
set => SetProperty(ref _description, value ?? string.Empty);
}
/// <summary>控件类型Text / Number / Choice / FilePath</summary>
public ParameterSetFieldType FieldType
{
get => _fieldType;
set => SetProperty(ref _fieldType, value);
}
/// <summary>下拉选项Choice 类型时使用)</summary>
public List<string> Options { get; set; } = new List<string>();
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
/// <summary>
/// 参数集特殊消息 - 用于在聊天流中展示一组「名称 / 值 / 描述」参数(如网格化参数等),由基本消息样式组合而成。
/// </summary>
public class ParameterSetMessage : ISpecialMessage, INotifyPropertyChanged
{
private string _title = "参数";
public string Id { get; set; } = Guid.NewGuid().ToString();
public string TypeName => "ParameterSet";
public bool IsLive => false;
public string Title
{
get => _title;
set => SetProperty(ref _title, value ?? string.Empty);
}
/// <summary>参数项列表(名称、值、描述)</summary>
public ObservableCollection<ParameterSetItem> Items { get; } = new ObservableCollection<ParameterSetItem>();
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// 从 YAML 文本填充参数集。格式示例:
/// Input:
/// Value: ...
/// Description: 散点数据文件路径
/// Type: FilePath
/// 支持 PascalCase / camelCase 键Value/value, Type/type 等)。
/// </summary>
public void LoadFromJson(string text)
{
Items.Clear();
if (string.IsNullOrWhiteSpace(text)) return;
LoadFromYamlInternal(text.Trim());
}
/// <summary>
/// 从 YAML 解析参数集。根级为参数名(如 Input, Faultage每项含 Value/Description/Type/Options。
/// 支持根级单键包装(如 parameters: 下再挂 Input/Faultage/...),会自动解包一层。
/// </summary>
private void LoadFromYamlInternal(string yaml)
{
try
{
var deserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.Build();
var root = deserializer.Deserialize<Dictionary<string, object>>(yaml);
if (root == null || root.Count == 0) return;
// 若根级只有一键且其值为映射,认为是 parameters: { input: ..., faultage: ... },用内层作为参数表
IEnumerable<KeyValuePair<string, object>> paramsIter = root;
if (root.Count == 1)
{
var singleValue = root.Values.First();
if (singleValue is Dictionary<object, object> inner)
{
var unwrapped = new Dictionary<string, object>();
foreach (var e in inner)
unwrapped[e.Key?.ToString() ?? ""] = e.Value;
paramsIter = unwrapped;
}
else if (singleValue is IDictionary<object, object> innerDict)
{
var unwrapped = new Dictionary<string, object>();
foreach (var e in innerDict)
unwrapped[e.Key?.ToString() ?? ""] = e.Value;
paramsIter = unwrapped;
}
else if (singleValue is Dictionary<string, object> innerStr)
{
paramsIter = innerStr;
}
}
foreach (var kvp in paramsIter)
{
var name = kvp.Key ?? "";
if (string.IsNullOrWhiteSpace(name)) continue;
var entry = ParseYamlEntry(kvp.Value, name);
string valueText = entry.valueText;
string description = entry.description;
var fieldType = entry.fieldType;
var options = entry.options;
if (fieldType == ParameterSetFieldType.Choice && options.Count == 0)
{
options.AddRange(GetDefaultOptionsForChoice(name));
}
if (string.Equals(name, "Input", StringComparison.OrdinalIgnoreCase) && fieldType == ParameterSetFieldType.Text)
{
fieldType = ParameterSetFieldType.FilePath;
}
Items.Add(new ParameterSetItem
{
Name = name,
ValueText = valueText,
Description = description,
FieldType = fieldType,
Options = options,
});
}
}
catch
{
// 解析失败时保持空列表
}
}
private static (string valueText, string description, ParameterSetFieldType fieldType, List<string> options) ParseYamlEntry(object? raw, string name)
{
string valueText = string.Empty;
string description = string.Empty;
var fieldType = ParameterSetFieldType.Text;
var options = new List<string>();
Dictionary<object, object>? dictObj = raw as Dictionary<object, object>;
Dictionary<string, object>? dictStr = raw as Dictionary<string, object>;
if (dictObj == null && dictStr == null)
{
return (valueText, description, fieldType, options);
}
// 支持 PascalCaseValue/Type/Options与 camelCasevalue/type/options
if (TryGetValueIgnoreCase(raw, "value", out var valObj))
{
valueText = valObj == null ? "" : (valObj is bool b ? (b ? "true" : "false") : valObj.ToString() ?? "");
}
if (TryGetValueIgnoreCase(raw, "description", out var descObj) && descObj != null)
{
description = descObj.ToString() ?? "";
}
if (TryGetValueIgnoreCase(raw, "type", out var typeObj) && typeObj != null)
{
fieldType = ParseTypeString(typeObj.ToString() ?? "");
}
else
{
fieldType = InferFieldType(name, valueText);
}
if (TryGetValueIgnoreCase(raw, "options", out var optObj))
{
if (optObj is IEnumerable<object> optList)
{
foreach (var o in optList)
{
if (o?.ToString() is string s)
{
options.Add(s);
}
}
}
else if (optObj is System.Collections.IEnumerable optEnum)
{
foreach (var item in optEnum)
{
if (item?.ToString() is string optStr)
{
options.Add(optStr);
}
}
}
}
if (fieldType == ParameterSetFieldType.Text &&
(description.IndexOf("文件路径", StringComparison.OrdinalIgnoreCase) >= 0 ||
(description.IndexOf("文件", StringComparison.OrdinalIgnoreCase) >= 0 && description.IndexOf("路径", StringComparison.OrdinalIgnoreCase) >= 0)))
{
fieldType = ParameterSetFieldType.FilePath;
}
return (valueText, description, fieldType, options);
}
private static bool TryGetValueIgnoreCase(object? dict, string key, out object? value)
{
value = null;
if (dict == null)
{
return false;
}
var keyEq = StringComparer.OrdinalIgnoreCase;
if (dict is Dictionary<object, object> objDict)
{
foreach (var kv in objDict)
{
if (keyEq.Equals(kv.Key?.ToString(), key))
{
value = kv.Value;
return true;
}
}
return false;
}
if (dict is Dictionary<string, object> strDict)
{
foreach (var kv in strDict)
{
if (keyEq.Equals(kv.Key, key))
{
value = kv.Value;
return true;
}
}
return false;
}
return false;
}
private static ParameterSetFieldType ParseTypeString(string typeStr)
{
switch (typeStr.Trim().ToLowerInvariant())
{
case "choice": return ParameterSetFieldType.Choice;
case "number": return ParameterSetFieldType.Number;
case "filepath":
case "file": return ParameterSetFieldType.FilePath;
case "boolean":
case "bool": return ParameterSetFieldType.Text;
default: return ParameterSetFieldType.Text;
}
}
/// <summary>根据键名和当前值推断控件类型</summary>
private static ParameterSetFieldType InferFieldType(string name, string valueText)
{
if (name.IndexOf("成图算法", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("算法", StringComparison.OrdinalIgnoreCase) >= 0)
return ParameterSetFieldType.Choice;
if (name.IndexOf("文件", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("路径", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("path", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("file", StringComparison.OrdinalIgnoreCase) >= 0)
return ParameterSetFieldType.FilePath;
if (name.IndexOf("步长", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("范围", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("min", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("max", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("列", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("行", StringComparison.OrdinalIgnoreCase) >= 0)
return ParameterSetFieldType.Number;
if (double.TryParse(valueText?.Trim(), System.Globalization.NumberStyles.Any, null, out _))
return ParameterSetFieldType.Number;
return ParameterSetFieldType.Text;
}
/// <summary>成图算法等键的默认下拉选项</summary>
private static IEnumerable<string> GetDefaultOptionsForChoice(string name)
{
if (name.IndexOf("成图算法", StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf("算法", StringComparison.OrdinalIgnoreCase) >= 0)
return new[] { "IDW", "Kriging", "自然邻点", "反距离加权" };
return Array.Empty<string>();
}
}
}