Scripts/Editor/Utility/WitUnderstandingViewer.cs (447 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ using System; using System.Collections.Generic; using System.IO; using Facebook.WitAi.CallbackHandlers; using Facebook.WitAi.Configuration; using Facebook.WitAi.Data; using Facebook.WitAi.Data.Configuration; using Facebook.WitAi.Lib; using Facebook.WitAi.Utilities; using UnityEditor; using UnityEngine; namespace Facebook.WitAi.Windows { public class WitUnderstandingViewer : WitConfigurationWindow { [SerializeField] private Texture2D witHeader; [SerializeField] private string responseText; private string utterance; private WitResponseNode response; private Dictionary<string, bool> foldouts; private DateTime submitStart; private TimeSpan requestLength; private string status; private VoiceService wit; private int responseCode; private WitRequest request; private int savePopup; private GUIStyle hamburgerButton; public bool HasWit => null != wit; class Content { public static GUIContent copyPath; public static GUIContent copyCode; public static GUIContent createStringValue; public static GUIContent createIntValue; public static GUIContent createFloatValue; static Content() { createStringValue = new GUIContent("Create Value Reference/Create String"); createIntValue = new GUIContent("Create Value Reference/Create Int"); createFloatValue = new GUIContent("Create Value Reference/Create Float"); copyPath = new GUIContent("Copy Path to Clipboard"); copyCode = new GUIContent("Copy Code to Clipboard"); } } protected override GUIContent Title => WitTexts.UnderstandingTitleContent; protected override WitTexts.WitAppEndpointType HeaderEndpointType => WitTexts.WitAppEndpointType.Understanding; protected override void OnEnable() { base.OnEnable(); EditorApplication.playModeStateChanged += OnPlayModeStateChanged; SetWit(GameObject.FindObjectOfType<VoiceService>()); if (!string.IsNullOrEmpty(responseText)) { response = WitResponseNode.Parse(responseText); } status = WitTexts.Texts.UnderstandingViewerPromptLabel; } protected override void OnDisable() { EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; } private void OnPlayModeStateChanged(PlayModeStateChange state) { if (state == PlayModeStateChange.EnteredPlayMode && !HasWit) { SetWit(FindObjectOfType<VoiceService>()); } } private void OnSelectionChange() { if (Selection.activeGameObject) { wit = Selection.activeGameObject.GetComponent<VoiceService>(); SetWit(wit); } } private void SetWit(VoiceService wit) { if (HasWit) { wit.events.OnRequestCreated.RemoveListener(OnRequestCreated); wit.events.OnError.RemoveListener(OnError); wit.events.OnResponse.RemoveListener(ShowResponse); wit.events.OnFullTranscription.RemoveListener(ShowTranscription); wit.events.OnPartialTranscription.RemoveListener(ShowTranscription); } if (null != wit) { this.wit = wit; wit.events.OnRequestCreated.AddListener(OnRequestCreated); wit.events.OnError.AddListener(OnError); wit.events.OnResponse.AddListener(ShowResponse); wit.events.OnFullTranscription.AddListener(ShowTranscription); wit.events.OnPartialTranscription.AddListener(ShowTranscription); // We will be measuring perceived request time since the actual request starts // as soon as the mic goes active and the user says something. wit.events.OnStoppedListening.AddListener(ResetStartTime); Repaint(); } } private void ResetStartTime() { submitStart = System.DateTime.Now; } private void OnError(string title, string message) { status = message; } private void OnRequestCreated(WitRequest request) { this.request = request; ResetStartTime(); } private void ShowTranscription(string transcription) { utterance = transcription; Repaint(); } // On gui protected override void OnGUI() { base.OnGUI(); EditorGUILayout.BeginHorizontal(); WitEditorUI.LayoutStatusLabel(status); GUILayout.BeginVertical(GUILayout.Width(24)); GUILayout.Space(4); GUILayout.BeginHorizontal(); GUILayout.Space(4); var rect = GUILayoutUtility.GetLastRect(); if (null == hamburgerButton) { // GUI.skin must be called from OnGUI hamburgerButton = new GUIStyle(GUI.skin.GetStyle("PaneOptions")); hamburgerButton.imagePosition = ImagePosition.ImageOnly; } var value = EditorGUILayout.Popup(-1, new string[] {"Save", "Copy to Clipboard"}, hamburgerButton, GUILayout.Width(24)); if (-1 != value) { if (value == 0) { var path = EditorUtility.SaveFilePanel("Save Response Json", Application.dataPath, "result", "json"); if (!string.IsNullOrEmpty(path)) { File.WriteAllText(path, response.ToString()); } } else { EditorGUIUtility.systemCopyBuffer = response.ToString(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); } protected override void LayoutContent() { // Layout wit select base.LayoutContent(); // Need configuration if (!witConfiguration) { WitEditorUI.LayoutErrorLabel(WitTexts.Texts.UnderstandingViewerMissingConfigLabel); return; } // Need app id string clientAccessToken = witConfiguration.clientAccessToken; if (string.IsNullOrEmpty(clientAccessToken)) { WitEditorUI.LayoutErrorLabel(WitTexts.Texts.UnderstandingViewerNoAppLabel); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (WitEditorUI.LayoutTextButton(WitTexts.Texts.UnderstandingViewerSettingsButtonLabel)) { Selection.activeObject = witConfiguration; } GUILayout.EndHorizontal(); return; } bool updated = false; bool allowInput = !wit || !wit.Active; GUI.enabled = allowInput; WitEditorUI.LayoutTextField(new GUIContent(WitTexts.Texts.UnderstandingViewerUtteranceLabel), ref utterance, ref updated); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (allowInput && WitEditorUI.LayoutTextButton(WitTexts.Texts.UnderstandingViewerSubmitButtonLabel)) { responseText = ""; if (!string.IsNullOrEmpty(utterance)) { SubmitUtterance(); } else { response = null; } } GUI.enabled = true; if (EditorApplication.isPlaying && wit) { if (!wit.Active && WitEditorUI.LayoutTextButton(WitTexts.Texts.UnderstandingViewerActivateButtonLabel)) { wit.Activate(); } if (wit.Active && WitEditorUI.LayoutTextButton(WitTexts.Texts.UnderstandingViewerDeactivateButtonLabel)) { wit.Deactivate(); } if (wit.Active && WitEditorUI.LayoutTextButton(WitTexts.Texts.UnderstandingViewerAbortButtonLabel)) { wit.DeactivateAndAbortRequest(); } } GUILayout.EndHorizontal(); // Results GUILayout.BeginVertical(EditorStyles.helpBox); if (wit && wit.MicActive) { WitEditorUI.LayoutWrapLabel(WitTexts.Texts.UnderstandingViewerListeningLabel); } else if (wit && wit.IsRequestActive) { WitEditorUI.LayoutWrapLabel(WitTexts.Texts.UnderstandingViewerLoadingLabel); } else if (response != null) { DrawResponse(); } else if (string.IsNullOrEmpty(responseText)) { WitEditorUI.LayoutWrapLabel(WitTexts.Texts.UnderstandingViewerPromptLabel); } else { WitEditorUI.LayoutWrapLabel(responseText); } GUILayout.FlexibleSpace(); GUILayout.EndVertical(); } private void SubmitUtterance() { if (Application.isPlaying && !HasWit) { SetDefaultWit(); } // Remove response response = null; if (wit && Application.isPlaying) { status = WitTexts.Texts.UnderstandingViewerListeningLabel; responseText = status; wit.Activate(utterance); // Hack to watch for loading to complete. Response does not // come back on the main thread so Repaint in onResponse in // the editor does nothing. EditorApplication.update += WatchForWitResponse; } else { status = WitTexts.Texts.UnderstandingViewerLoadingLabel; responseText = status; submitStart = System.DateTime.Now; request = witConfiguration.MessageRequest(utterance, new WitRequestOptions()); request.onResponse = OnResponse; request.Request(); } } private void WatchForWitResponse() { if (wit && !wit.Active) { Repaint(); EditorApplication.update -= WatchForWitResponse; } } private void SetDefaultWit() { SetWit(FindObjectOfType<VoiceService>()); } private void OnResponse(WitRequest request) { responseCode = request.StatusCode; if (null != request.ResponseData) { ShowResponse(request.ResponseData); } else if (!string.IsNullOrEmpty(request.StatusDescription)) { responseText = request.StatusDescription; } else { responseText = "No response. Status: " + request.StatusCode; } } private void ShowResponse(WitResponseNode r) { response = r; responseText = response.ToString(); requestLength = DateTime.Now - submitStart; status = $"Response time: {requestLength}"; } private void DrawResponse() { DrawResponseNode(response); } private void DrawResponseNode(WitResponseNode witResponseNode, string path = "") { if (null == witResponseNode?.AsObject) return; if(string.IsNullOrEmpty(path)) DrawNode(witResponseNode["text"], "text", path); var names = witResponseNode.AsObject.ChildNodeNames; Array.Sort(names); foreach (string child in names) { if (!(string.IsNullOrEmpty(path) && child == "text")) { var childNode = witResponseNode[child]; DrawNode(childNode, child, path); } } } private void DrawNode(WitResponseNode childNode, string child, string path, bool isArrayElement = false) { if (childNode == null) { return; } string childPath; if (path.Length > 0) { childPath = isArrayElement ? $"{path}[{child}]" : $"{path}.{child}"; } else { childPath = child; } if (!string.IsNullOrEmpty(childNode.Value)) { GUILayout.BeginHorizontal(); GUILayout.Space(15 * EditorGUI.indentLevel); if (GUILayout.Button($"{child} = {childNode.Value}", "Label")) { ShowNodeMenu(childNode, childPath); } GUILayout.EndHorizontal(); } else { var childObject = childNode.AsObject; var childArray = childNode.AsArray; if ((null != childObject || null != childArray) && Foldout(childPath, child)) { EditorGUI.indentLevel++; if (null != childObject) { DrawResponseNode(childNode, childPath); } if (null != childArray) { DrawArray(childArray, childPath); } EditorGUI.indentLevel--; } } } private void ShowNodeMenu(WitResponseNode node, string path) { GenericMenu menu = new GenericMenu(); menu.AddItem(Content.createStringValue, false, () => WitDataCreation.CreateStringValue(path)); menu.AddItem(Content.createIntValue, false, () => WitDataCreation.CreateIntValue(path)); menu.AddItem(Content.createFloatValue, false, () => WitDataCreation.CreateFloatValue(path)); menu.AddSeparator(""); menu.AddItem(Content.copyPath, false, () => { EditorGUIUtility.systemCopyBuffer = path; }); menu.AddItem(Content.copyCode, false, () => { EditorGUIUtility.systemCopyBuffer = WitResultUtilities.GetCodeFromPath(path); }); if (Selection.activeGameObject) { menu.AddSeparator(""); var label = new GUIContent($"Add response matcher to {Selection.activeObject.name}"); menu.AddItem(label, false, () => { var valueHandler = Selection.activeGameObject.AddComponent<WitResponseMatcher>(); valueHandler.intent = response.GetIntentName(); valueHandler.valueMatchers = new ValuePathMatcher[] { new ValuePathMatcher() { path = path } }; }); AddMultiValueUpdateItems(path, menu); } menu.ShowAsContext(); } private void AddMultiValueUpdateItems(string path, GenericMenu menu) { string name = path; int index = path.LastIndexOf('.'); if (index > 0) { name = name.Substring(index + 1); } var mvhs = Selection.activeGameObject.GetComponents<WitResponseMatcher>(); if (mvhs.Length > 1) { for (int i = 0; i < mvhs.Length; i++) { var handler = mvhs[i]; menu.AddItem( new GUIContent($"Add {name} matcher to {Selection.activeGameObject.name}/Handler {(i + 1)}"), false, (h) => AddNewEventHandlerPath((WitResponseMatcher) h, path), handler); } } else if (mvhs.Length == 1) { var handler = mvhs[0]; menu.AddItem( new GUIContent($"Add {name} matcher to {Selection.activeGameObject.name}'s Response Matcher"), false, (h) => AddNewEventHandlerPath((WitResponseMatcher) h, path), handler); } } private void AddNewEventHandlerPath(WitResponseMatcher handler, string path) { Array.Resize(ref handler.valueMatchers, handler.valueMatchers.Length + 1); handler.valueMatchers[handler.valueMatchers.Length - 1] = new ValuePathMatcher() { path = path }; } private void DrawArray(WitResponseArray childArray, string childPath) { for (int i = 0; i < childArray.Count; i++) { DrawNode(childArray[i], i.ToString(), childPath, true); } } private bool Foldout(string path, string label) { if (null == foldouts) foldouts = new Dictionary<string, bool>(); if (!foldouts.TryGetValue(path, out var state)) { state = false; foldouts[path] = state; } var newState = EditorGUILayout.Foldout(state, label); if (newState != state) { foldouts[path] = newState; } return newState; } } }