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#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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>();
}
}
}