添加询问李诫(AI 问答)功能
- 添加 AIChatComponent:用于主要的逻辑(构造请求,解析响应) - 添加 AIChatForm:用于问答的页面展示
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class AIChatForm : UGuiForm
|
||||
{
|
||||
[SerializeField] private TMP_Text _returnButtonText;
|
||||
|
||||
[SerializeField] private TMP_InputField _inputField;
|
||||
|
||||
[SerializeField] private RectTransform _dialogContent;
|
||||
|
||||
[SerializeField] private TMP_Text _titleText;
|
||||
|
||||
[SerializeField] private ScrollRect _historyScrollRect;
|
||||
|
||||
[SerializeField] private AIDialogItem _aiDialogItemPrefab;
|
||||
|
||||
[SerializeField] private PlayerDialogItem _playerDialogItemPrefab;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _languageSelectGroup;
|
||||
|
||||
[SerializeField] private string _returnButtonNormalText = "<sprite name=\"KEYBOARD_Esc\"> Back";
|
||||
|
||||
[SerializeField] private string _returnButtonHoverText = "<sprite name=\"KEYBOARD_Esc\"><u> Back </u>";
|
||||
|
||||
private readonly List<Component> _dialogItems = new List<Component>();
|
||||
private AIChatFormController _controller;
|
||||
private VerticalLayoutGroup _contentLayoutGroup;
|
||||
|
||||
public Vector2 ContentSize => _dialogContent != null
|
||||
? new Vector2(_dialogContent.rect.width, _dialogContent.rect.height)
|
||||
: Vector2.zero;
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
EnsureReferences();
|
||||
|
||||
if (_inputField != null)
|
||||
{
|
||||
_inputField.onSubmit.RemoveListener(OnInputFieldSubmitted);
|
||||
_inputField.onSubmit.AddListener(OnInputFieldSubmitted);
|
||||
}
|
||||
|
||||
if (!(userData is AIChatFormContext context))
|
||||
{
|
||||
Log.Error("AIChatFormContext is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshUI(context);
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
if (_inputField != null)
|
||||
{
|
||||
_inputField.onSubmit.RemoveListener(OnInputFieldSubmitted);
|
||||
}
|
||||
|
||||
ClearDialogItems();
|
||||
_dialogItems.Clear();
|
||||
_controller = null;
|
||||
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
public void RefreshUI(AIChatFormContext context)
|
||||
{
|
||||
EnsureReferences();
|
||||
if (context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controller = context.Controller;
|
||||
|
||||
if (_titleText != null && !string.IsNullOrEmpty(context.Title))
|
||||
{
|
||||
_titleText.text = context.Title;
|
||||
}
|
||||
|
||||
if (_languageSelectGroup != null)
|
||||
{
|
||||
int targetLanguageMode = Mathf.Max(0, context.LanguageMode);
|
||||
try
|
||||
{
|
||||
_languageSelectGroup.SetValue(targetLanguageMode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_languageSelectGroup.SetValue(0);
|
||||
context.LanguageMode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.ClearHistoryOnOpen || _dialogItems.Count == 0)
|
||||
{
|
||||
ClearDialogItems();
|
||||
BuildHistory(context.Messages);
|
||||
}
|
||||
|
||||
FocusInputField();
|
||||
}
|
||||
|
||||
public int AppendAIDialog(AIDialogItemContext context)
|
||||
{
|
||||
EnsureReferences();
|
||||
if (_aiDialogItemPrefab == null || _dialogContent == null)
|
||||
{
|
||||
Log.Warning("Append AI dialog failed. Missing AI item prefab or content root.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
AIDialogItem item = Instantiate(_aiDialogItemPrefab, _dialogContent, false);
|
||||
item.gameObject.SetActive(true);
|
||||
item.OnInit(context);
|
||||
_dialogItems.Add(item);
|
||||
ScrollToBottom();
|
||||
return _dialogItems.Count - 1;
|
||||
}
|
||||
|
||||
public void UpdateAIDialog(int itemIndex, AIDialogItemContext context)
|
||||
{
|
||||
if (itemIndex < 0 || itemIndex >= _dialogItems.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(_dialogItems[itemIndex] is AIDialogItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.OnInit(context);
|
||||
ScrollToBottom();
|
||||
}
|
||||
|
||||
public int AppendPlayerDialog(PlayerDialogItemContext context)
|
||||
{
|
||||
EnsureReferences();
|
||||
if (_playerDialogItemPrefab == null || _dialogContent == null)
|
||||
{
|
||||
Log.Warning("Append player dialog failed. Missing player item prefab or content root.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
PlayerDialogItem item = Instantiate(_playerDialogItemPrefab, _dialogContent, false);
|
||||
item.gameObject.SetActive(true);
|
||||
item.OnInit(context);
|
||||
_dialogItems.Add(item);
|
||||
ScrollToBottom();
|
||||
return _dialogItems.Count - 1;
|
||||
}
|
||||
|
||||
public void SetInputInteractable(bool interactable)
|
||||
{
|
||||
if (_inputField == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_inputField.interactable = interactable;
|
||||
}
|
||||
|
||||
public void FocusInputField()
|
||||
{
|
||||
if (_inputField == null || !_inputField.interactable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_inputField.Select();
|
||||
_inputField.ActivateInputField();
|
||||
}
|
||||
|
||||
public void OnCommitButtonClick()
|
||||
{
|
||||
CommitInput();
|
||||
}
|
||||
|
||||
public void OnReturnButtonHover()
|
||||
{
|
||||
if (_returnButtonText != null)
|
||||
{
|
||||
_returnButtonText.text = _returnButtonHoverText;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReturnButtonClick()
|
||||
{
|
||||
_controller?.CloseUI();
|
||||
}
|
||||
|
||||
public void OnReturnButtonHoverEnd()
|
||||
{
|
||||
if (_returnButtonText != null)
|
||||
{
|
||||
_returnButtonText.text = _returnButtonNormalText;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureReferences()
|
||||
{
|
||||
if (_historyScrollRect == null)
|
||||
{
|
||||
_historyScrollRect = GetComponentInChildren<ScrollRect>(true);
|
||||
}
|
||||
|
||||
if (_historyScrollRect != null && _dialogContent == null)
|
||||
{
|
||||
_dialogContent = _historyScrollRect.content;
|
||||
}
|
||||
|
||||
if (_dialogContent != null)
|
||||
{
|
||||
_contentLayoutGroup = _dialogContent.GetComponent<VerticalLayoutGroup>();
|
||||
if (_contentLayoutGroup == null)
|
||||
{
|
||||
_contentLayoutGroup = _dialogContent.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
}
|
||||
|
||||
_contentLayoutGroup.enabled = true;
|
||||
_contentLayoutGroup.childAlignment = TextAnchor.UpperLeft;
|
||||
_contentLayoutGroup.childForceExpandHeight = false;
|
||||
_contentLayoutGroup.childForceExpandWidth = true;
|
||||
_contentLayoutGroup.childControlHeight = true;
|
||||
_contentLayoutGroup.childControlWidth = true;
|
||||
_contentLayoutGroup.spacing = 12f;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildHistory(List<AIChatMessageContext> messages)
|
||||
{
|
||||
if (messages == null || messages.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < messages.Count; i++)
|
||||
{
|
||||
AIChatMessageContext message = messages[i];
|
||||
if (message == null || string.IsNullOrEmpty(message.Content))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.IsPlayer)
|
||||
{
|
||||
AppendPlayerDialog(new PlayerDialogItemContext
|
||||
{
|
||||
Content = message.Content
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendAIDialog(new AIDialogItemContext
|
||||
{
|
||||
ParentWidth = ContentSize.x,
|
||||
Content = message.Content
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CommitInput()
|
||||
{
|
||||
if (_controller == null || _inputField == null || !_inputField.interactable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string content = _inputField.text;
|
||||
_inputField.text = string.Empty;
|
||||
_controller.SubmitPlayerMessage(content, GetLanguageMode());
|
||||
FocusInputField();
|
||||
}
|
||||
|
||||
private void OnInputFieldSubmitted(string submittedText)
|
||||
{
|
||||
CommitInput();
|
||||
}
|
||||
|
||||
private int GetLanguageMode()
|
||||
{
|
||||
return _languageSelectGroup != null ? _languageSelectGroup.GetIntValue() : 0;
|
||||
}
|
||||
|
||||
private void ClearDialogItems()
|
||||
{
|
||||
if (_dialogContent == null)
|
||||
{
|
||||
_dialogItems.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = _dialogContent.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Transform child = _dialogContent.GetChild(i);
|
||||
if (child != null)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
_dialogItems.Clear();
|
||||
}
|
||||
|
||||
private void ScrollToBottom()
|
||||
{
|
||||
if (_historyScrollRect == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCoroutine(nameof(ScrollToBottomAtEndOfFrame));
|
||||
StartCoroutine(nameof(ScrollToBottomAtEndOfFrame));
|
||||
}
|
||||
|
||||
private IEnumerator ScrollToBottomAtEndOfFrame()
|
||||
{
|
||||
yield return null;
|
||||
Canvas.ForceUpdateCanvases();
|
||||
_historyScrollRect.verticalNormalizedPosition = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e59af4c8fe70e184ea5ab8d33fe39c4b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,90 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class AIDialogItem : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private RectTransform _rectTransform;
|
||||
|
||||
[SerializeField] private TMP_Text _contentText;
|
||||
|
||||
private LayoutElement _layoutElement;
|
||||
private float _minHeight;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CacheReferences();
|
||||
}
|
||||
|
||||
public void OnInit(AIDialogItemContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnInit(context.ParentWidth, context.Content);
|
||||
}
|
||||
|
||||
public void OnInit(float parentWidth, string content)
|
||||
{
|
||||
CacheReferences();
|
||||
if (_rectTransform == null || _contentText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_contentText.text = content ?? string.Empty;
|
||||
|
||||
if (parentWidth > 0f)
|
||||
{
|
||||
_rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, parentWidth);
|
||||
}
|
||||
|
||||
float preferredHeight = _contentText.GetPreferredValues(_contentText.text, _rectTransform.rect.width,
|
||||
float.PositiveInfinity).y;
|
||||
float targetHeight = Mathf.Max(_minHeight, preferredHeight);
|
||||
|
||||
if (_layoutElement != null)
|
||||
{
|
||||
_layoutElement.preferredHeight = targetHeight;
|
||||
}
|
||||
|
||||
_rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, targetHeight);
|
||||
}
|
||||
|
||||
public void SetContent(string content, float parentWidth)
|
||||
{
|
||||
OnInit(parentWidth, content);
|
||||
}
|
||||
|
||||
private void CacheReferences()
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
{
|
||||
_rectTransform = transform as RectTransform;
|
||||
}
|
||||
|
||||
if (_contentText == null)
|
||||
{
|
||||
_contentText = GetComponentInChildren<TMP_Text>(true);
|
||||
}
|
||||
|
||||
if (_layoutElement == null && _rectTransform != null)
|
||||
{
|
||||
_layoutElement = gameObject.GetComponent<LayoutElement>();
|
||||
if (_layoutElement == null)
|
||||
{
|
||||
_layoutElement = gameObject.AddComponent<LayoutElement>();
|
||||
}
|
||||
}
|
||||
|
||||
if (_rectTransform != null && _minHeight <= 0f)
|
||||
{
|
||||
_minHeight = Mathf.Max(20f, _rectTransform.rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9fe74c6a0acb0a40bae49bdf039be60
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -10,7 +10,6 @@ namespace UI
|
||||
/// 可拖拽拼装部件:负责拖拽交互、出生点回退与放置到槽位。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
[DisallowMultipleComponent]
|
||||
public class CombineDraggablePart : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
#region Inspector Config
|
||||
|
||||
@@ -4,7 +4,6 @@ using Event;
|
||||
using GameFramework.Event;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
@@ -12,7 +11,6 @@ namespace UI
|
||||
/// <summary>
|
||||
/// MVC UI form for GameplayA. It builds slots and draggable parts from external data.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class CombineForm : UGuiForm
|
||||
{
|
||||
#region Property
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace UI
|
||||
/// <summary>
|
||||
/// 拼装槽位:接收拖拽部件并转发给玩法控制器做规则校验。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class CombineSlot : MonoBehaviour, IDropHandler
|
||||
{
|
||||
#region Inspector Config
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class PlayerDialogItem : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_Text _contentText;
|
||||
|
||||
[SerializeField] private Image _bg;
|
||||
|
||||
[SerializeField] private float _preferredWidth;
|
||||
|
||||
[SerializeField] private float _horizontalPadding;
|
||||
|
||||
[SerializeField] private float _verticalPadding;
|
||||
|
||||
private RectTransform _rootRectTransform;
|
||||
private LayoutElement _layoutElement;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
CacheReferences();
|
||||
}
|
||||
|
||||
public void OnInit(PlayerDialogItemContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnInit(context.Content);
|
||||
}
|
||||
|
||||
public void OnInit(string content)
|
||||
{
|
||||
CacheReferences();
|
||||
|
||||
if (_contentText == null || _bg == null || _rootRectTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_contentText.text = content ?? string.Empty;
|
||||
|
||||
Vector2 unconstrainedSize = _contentText.GetPreferredValues(_contentText.text, float.PositiveInfinity,
|
||||
float.PositiveInfinity);
|
||||
float targetWidth = Mathf.Min(_preferredWidth, unconstrainedSize.x + _horizontalPadding);
|
||||
targetWidth = Mathf.Max(_horizontalPadding, targetWidth);
|
||||
|
||||
float textWidth = Mathf.Max(0f, targetWidth - _horizontalPadding);
|
||||
Vector2 constrainedSize = _contentText.GetPreferredValues(_contentText.text, textWidth,
|
||||
float.PositiveInfinity);
|
||||
float targetHeight = Mathf.Max(0f, constrainedSize.y + _verticalPadding);
|
||||
|
||||
RectTransform bgRect = _bg.rectTransform;
|
||||
bgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, targetWidth);
|
||||
bgRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, targetHeight);
|
||||
|
||||
if (_layoutElement != null)
|
||||
{
|
||||
_layoutElement.preferredHeight = targetHeight;
|
||||
_layoutElement.minHeight = targetHeight;
|
||||
}
|
||||
|
||||
_rootRectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, targetHeight);
|
||||
}
|
||||
|
||||
private void CacheReferences()
|
||||
{
|
||||
if (_rootRectTransform == null)
|
||||
{
|
||||
_rootRectTransform = transform as RectTransform;
|
||||
}
|
||||
|
||||
if (_contentText == null)
|
||||
{
|
||||
_contentText = GetComponentInChildren<TMP_Text>(true);
|
||||
}
|
||||
|
||||
if (_bg == null)
|
||||
{
|
||||
_bg = GetComponentInChildren<Image>(true);
|
||||
}
|
||||
|
||||
if (_layoutElement == null && _rootRectTransform != null)
|
||||
{
|
||||
_layoutElement = gameObject.GetComponent<LayoutElement>();
|
||||
if (_layoutElement == null)
|
||||
{
|
||||
_layoutElement = gameObject.AddComponent<LayoutElement>();
|
||||
}
|
||||
}
|
||||
|
||||
if (_contentText == null || _bg == null || _rootRectTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RectTransform bgRect = _bg.rectTransform;
|
||||
RectTransform textRect = _contentText.rectTransform;
|
||||
|
||||
_horizontalPadding = Mathf.Max(0f, bgRect.rect.width - textRect.rect.width);
|
||||
_verticalPadding = Mathf.Max(0f, bgRect.rect.height - textRect.rect.height);
|
||||
|
||||
if (_preferredWidth <= 0f)
|
||||
{
|
||||
_preferredWidth = bgRect.rect.width > 0f ? bgRect.rect.width : _rootRectTransform.rect.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d82051b50b7801c4f86c71e77c353f77
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -12,6 +12,8 @@ namespace UI
|
||||
|
||||
[SerializeField] private float _fadeDuration;
|
||||
|
||||
[SerializeField] private bool _allowFade = true;
|
||||
|
||||
private Sequence _fadeSequence;
|
||||
|
||||
public UnityEvent _onSelect;
|
||||
@@ -21,17 +23,26 @@ namespace UI
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
KillFadeSequence();
|
||||
_fadeSequence = DOTween.Sequence();
|
||||
_fadeSequence.Append(_bgImage.DOFade(1, _fadeDuration));
|
||||
|
||||
if (_allowFade)
|
||||
{
|
||||
_fadeSequence = DOTween.Sequence();
|
||||
_fadeSequence.Append(_bgImage.DOFade(1, _fadeDuration));
|
||||
}
|
||||
|
||||
_onSelect.Invoke();
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
KillFadeSequence();
|
||||
|
||||
_fadeSequence = DOTween.Sequence();
|
||||
_fadeSequence.Append(_bgImage.DOFade(0, _fadeDuration));
|
||||
|
||||
if (_allowFade)
|
||||
{
|
||||
_fadeSequence = DOTween.Sequence();
|
||||
_fadeSequence.Append(_bgImage.DOFade(0, _fadeDuration));
|
||||
}
|
||||
|
||||
_onDeselect.Invoke();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user