|
|
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);
|
|
|
}
|
|
|
|
|
|
// 支持 PascalCase(Value/Type/Options)与 camelCase(value/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>();
|
|
|
}
|
|
|
}
|
|
|
}
|