window.py (16674B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\auraguidance\client\ui\window.py 2 import blue 3 import re 4 import uthread2 5 from auraguidance.client import aura_guidance_signals 6 from auraguidance.client.api import get_aura_guidance_repository 7 from auraguidance.client.errors import AuraGuidanceError, AuraGuidanceServiceUnavailableError 8 from auraguidance.client.logger import logger 9 from auraguidance.client.ui.const import AURA_CHAT_ICON_PATH, AURA_WINDOW_ICON_PATH, CHANNEL_KEYWORDS, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, GREETING_LABEL_PATH, HELPPOINTER_KEYWORDS, INPUT_HINT_LABEL_PATH, LOADING_LABEL_PATH, MAX_INPUT_HEIGHT, MAX_WINDOW_HEIGHT, MAX_WINDOW_WIDTH, MIN_INPUT_HEIGHT, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, NO_ASSESS_LABEL_PATH, SENDER_TYPE_PLAYER, SENDER_TYPE_SERVER, AURA_CHAT_WINDOW_ICON_PATH 10 from auraguidance.client.ui.entry import AuraSuggestionsEntry, ChatMessageEntry 11 from carbonui import AxisAlignment, PickState, TextAlign, TextColor, uiconst 12 from carbonui.control.scrollContainer import ScrollContainer 13 from carbonui.control.window import Window 14 from carbonui.primitives.container import Container 15 from carbonui.primitives.containerAutoSize import ContainerAutoSize 16 from carbonui.primitives.sprite import Sprite 17 from carbonui.text.styles import TextBody 18 from carbonui.uianimations import animations 19 from carbonui.uicore import uicore 20 from chat.client.util import parse_chat_message 21 from eve.client.script.ui.control.divider import Divider 22 from eve.client.script.ui.control.eveEditPlainText import EditPlainText 23 from eveaudio.ui import UIAudioManager 24 from localization import GetByLabel 25 from menu import MenuLabel 26 from uihighlighting.uniqueNameConst import GetUniqueNeocomPointerName 27 28 class AuraGuidanceWindow(Window): 29 __guid__ = 'AuraGuidanceWindow' 30 default_windowID = 'AuraGuidanceWindow' 31 default_width = DEFAULT_WINDOW_WIDTH 32 default_height = DEFAULT_WINDOW_HEIGHT 33 default_left = 0 34 default_top = 0 35 neocom_titleLabelPath = 'UI/Accessories/AuraGuidance/CaptionLabel' 36 default_captionLabelPath = 'UI/Accessories/AuraGuidance/WindowTitle' 37 default_descriptionLabelPath = 'UI/Accessories/AuraGuidance/DescriptionLabel' 38 default_iconNum = AURA_WINDOW_ICON_PATH 39 default_scope = uiconst.SCOPE_INGAME 40 default_isCompact = False 41 default_apply_content_padding = False 42 default_analyticID = 'aura_guidance_window' 43 intro_conversation_id = 4088 44 intro_conversation_audio_play = 'voc_aura_4088_2_alt02_play' 45 intro_conversation_audio_stop = 'voc_aura_4088_2_alt02_stop' 46 __notifyevents__ = ['OnClientEvent_ConversationContinueButtonClicked'] 47 48 def ApplyAttributes(self, attributes): 49 super(AuraGuidanceWindow, self).ApplyAttributes(attributes) 50 self.audio_mgr = UIAudioManager() 51 self.connect_signals() 52 self._is_waiting_globally = False 53 self._repository = attributes.Get('repository', get_aura_guidance_repository()) 54 self._suggestions_entry = None 55 self.conversation_svc = sm.GetService('conversationService') 56 self._message_entries = [] 57 self.icon = AURA_CHAT_WINDOW_ICON_PATH 58 self.SetMinSize((MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)) 59 self.SetMaxSize((MAX_WINDOW_WIDTH, MAX_WINDOW_HEIGHT)) 60 if self._repository is None or not self._repository.is_enabled(): 61 return 62 self._layout() 63 entry = self._add_message(sender_type=SENDER_TYPE_SERVER) 64 entry.set_response_text('Hello Captain. How can I help?', False) 65 self._update_suggestions_entry() 66 self._scroll_to_bottom() 67 68 def display_first_time_intro(self): 69 self.display = False 70 self.conversation_svc.show_conversation(conversation_id=self.intro_conversation_id, show_continue_button=True, should_close_on_continue=True, should_autoprogress=False, show_only_once=True) 71 uthread2.call_after_wallclocktime_delay(self._play_VO, delay=1.5) 72 sm.GetService('neocom').ToggleAuraGuidanceIndicator(enabled=False) 73 74 def _play_VO(self): 75 sm.GetService('audio').SendUIEvent(self.intro_conversation_audio_play) 76 77 def OnClientEvent_ConversationContinueButtonClicked(self, conversation_id): 78 if conversation_id == self.intro_conversation_id: 79 self.display = True 80 sm.GetService('audio').SendUIEvent(self.intro_conversation_audio_stop) 81 82 def _layout(self): 83 self.content.padding = (0, 0, 0, 0) 84 self._construct_input_area() 85 self._construct_message_area() 86 87 def _construct_message_area(self): 88 self._message_scroll = ScrollContainer(name='message_scroll_area', parent=self.content, align=uiconst.TOALL, padding=(8, 4, 8, 4)) 89 self._message_scroll.GetMenu = self.GetMenu 90 self._welcome_container = Container(name='welcome_container', parent=self._message_scroll, align=uiconst.TOTOP, height=150, display=False) 91 self._welcome_icon = Sprite(name='welcome_icon', parent=self._welcome_container, align=uiconst.CENTERTOP, width=64, height=64, top=20, texturePath=AURA_CHAT_ICON_PATH) 92 self._welcome_text = TextBody(name='welcome_text', parent=self._welcome_container, align=uiconst.CENTERTOP, width=280, top=90, textAlign=TextAlign.CENTER, text=GetByLabel(GREETING_LABEL_PATH), color=TextColor.SECONDARY) 93 94 def _construct_input_area(self): 95 self._input_container = ContainerAutoSize(name='input_container', parent=self.content, align=uiconst.Align.TOBOTTOM, alignMode=uiconst.Align.TOBOTTOM) 96 self._divider = Divider(parent=self.content, align=uiconst.Align.TOBOTTOM_NOPUSH, pickState=PickState.ON, show_line=True, cross_axis_alignment=AxisAlignment.CENTER, height=4) 97 self._char_counter = TextBody(name='char_counter', parent=self._input_container, align=uiconst.BOTTOMRIGHT, text='0/500', color=TextColor.SECONDARY, padRight=8, padBottom=4) 98 self._input_field = EditPlainText(name='input_field', parent=self._input_container, align=uiconst.Align.TOBOTTOM, height=MIN_INPUT_HEIGHT * 2, counterMax=500, countWithTags=True, customCounter=self._char_counter, OnReturn=self._on_send_message) 99 self._divider.Startup(self._input_field, attribute='height', xory='y', minValue=MIN_INPUT_HEIGHT, maxValue=MAX_INPUT_HEIGHT) 100 self._input_field.allowPrivateDrops = 1 101 self._input_field.OnChange = self._on_input_change 102 uicore.registry.SetFocus(self._input_field) 103 self._input_field.SetHintText(GetByLabel(INPUT_HINT_LABEL_PATH)) 104 self._input_field.OnReturn = self._on_send_message 105 self._previous_inputs = [] 106 self._previous_input_index = None 107 aura_guidance_signals.on_suggestion_clicked.connect(self.send_message) 108 109 def _on_input_change(self): 110 text_raw = self._input_field.GetValue() 111 current_len = len(text_raw) if text_raw else 0 112 if current_len > 500: 113 self._char_counter.color = TextColor.DANGER 114 else: 115 self._char_counter.color = TextColor.SECONDARY 116 117 def _show_welcome_state(self): 118 self._welcome_container.display = True 119 animations.FadeIn(self._welcome_container, duration=0.4, endVal=1.0) 120 121 def _hide_welcome_state(self): 122 if self._welcome_container.display: 123 animations.FadeOut(self._welcome_container, duration=0.3, callback=self._on_welcome_hidden) 124 125 def _on_welcome_hidden(self): 126 self._welcome_container.display = False 127 128 def _on_send_message(self): 129 if not self._repository.is_enabled() or self.destroyed or self._is_waiting_globally: 130 return 131 text_raw = self._input_field.GetValue() 132 if not text_raw: 133 return 134 if len(text_raw) > 500: 135 return 136 text = parse_chat_message(text_raw, truncate=False) 137 if not text or not text.strip(): 138 return 139 if self._welcome_container.display: 140 self._hide_welcome_state() 141 if not self._previous_inputs or self._previous_inputs[-1] != text: 142 self._previous_inputs.append(text) 143 self._previous_input_index = None 144 self._input_field.Disable() 145 self.audio_mgr.SendUIEvent('ai_thinking_play') 146 player_entry = self._add_message(sender_type=SENDER_TYPE_PLAYER) 147 player_entry.set_response_text(text_raw, False) 148 aura_entry = self._add_message(sender_type=SENDER_TYPE_SERVER) 149 self._is_waiting_globally = True 150 uthread2.call_after_wallclocktime_delay(tasklet_func=self._scroll_to_bottom, delay=0.1) 151 uthread2.start_tasklet(self._request_response, text, aura_entry) 152 self._animate_entry(aura_entry) 153 154 def _animate_entry(self, entry): 155 tasklet = uthread2.start_tasklet(self._waiting_animation, entry) 156 entry.thread_tasklet = tasklet 157 uthread2.call_after_wallclocktime_delay(entry.stop_animation, delay=10.0) 158 159 def _return_previous_input(self): 160 if not self._previous_inputs: 161 return 162 if self._previous_input_index is None: 163 self._previous_input_index = len(self._previous_inputs) - 1 164 elif self._previous_input_index > 0: 165 self._previous_input_index -= 1 166 self._input_field.SetValue(self._previous_inputs[self._previous_input_index]) 167 uicore.registry.SetFocus(self._input_field) 168 169 def _request_response(self, query, entry): 170 try: 171 if self._repository: 172 dynamic_formatted_message, is_fallback = self._repository.get_response(query) 173 if not self.destroyed: 174 self._input_field.SetValue('') 175 cerberus_response = dynamic_formatted_message.get_localized_text(language_id=None) 176 linked_response = self._inject_helppointer_links(cerberus_response) 177 linked_response = self._inject_channel_links(linked_response) 178 entry.set_response_text(linked_response, is_fallback) 179 uthread2.call_after_wallclocktime_delay(tasklet_func=self._scroll_to_bottom, delay=0.2) 180 if not is_fallback: 181 self.audio_mgr.SendUIEvent('ai_notify_play') 182 else: 183 self.audio_mgr.SendUIEvent('ai_error_play') 184 else: 185 raise RuntimeError('Aura Guidance repository is not available.') 186 except AuraGuidanceError as e: 187 logger.error('Failed to get Aura Guidance response: %s', str(e)) 188 entry.set_error_state() 189 self.audio_mgr.SendUIEvent('ai_error_play') 190 self._return_previous_input() 191 except AuraGuidanceServiceUnavailableError as e: 192 logger.error('Aura Guidance service is not available: %s', str(e)) 193 entry.set_error_state() 194 self.audio_mgr.SendUIEvent('ai_error_play') 195 except Exception as e: 196 logger.exception('Unexpected error while getting Aura Guidance response: %s', str(e)) 197 entry.set_error_state() 198 self.audio_mgr.SendUIEvent('ai_error_play') 199 self.on_availability_changed(False) 200 finally: 201 self._update_suggestions_entry() 202 self._input_field.Enable() 203 self._is_waiting_globally = False 204 205 def _inject_helppointer_links(self, response): 206 if not response: 207 return response 208 result = response 209 sorted_keywords = sorted(HELPPOINTER_KEYWORDS.keys(), key=len, reverse=True) 210 for keyword in sorted_keywords: 211 btn_id = HELPPOINTER_KEYWORDS[keyword] 212 pointer_name = GetUniqueNeocomPointerName(btn_id) 213 pattern = '(?<![<\\w])(?<!href=")(?<!/)\\b(' + re.escape(keyword) + ')\\b(?![^<]*>)' 214 215 def make_replacer(ptr_name): 216 217 def replacer(match): 218 original_text = match.group(1) 219 return '<a href="helpPointer:{pointer}">{text}</a>'.format(pointer=ptr_name, text=original_text) 220 221 return replacer 222 223 result = re.sub(pattern, make_replacer(pointer_name), result, count=1, flags=re.IGNORECASE) 224 225 return result 226 227 def _inject_channel_links(self, response): 228 if not response: 229 return response 230 result = response 231 sorted_keywords = sorted(CHANNEL_KEYWORDS.keys(), key=len, reverse=True) 232 for keyword in sorted_keywords: 233 channel_id = CHANNEL_KEYWORDS[keyword] 234 pattern = '(?<![<\\w])(?<!href=")(?<!/)\\b(' + re.escape(keyword) + ')\\b(?![^<]*>)' 235 236 def make_replacer(ch_id): 237 238 def replacer(match): 239 original_text = match.group(1) 240 return '<a href="joinChannel:{channel}">{text}</a>'.format(channel=ch_id, text=original_text) 241 242 return replacer 243 244 result = re.sub(pattern, make_replacer(channel_id), result, count=1, flags=re.IGNORECASE) 245 246 return result 247 248 def _waiting_animation(self, entry): 249 loading_symbol = entry.message_text[-1] 250 while entry._is_waiting and not entry.destroyed: 251 entry.message_text += loading_symbol 252 if len(entry.message_text) > len(GetByLabel(LOADING_LABEL_PATH)) + 3: 253 entry.message_text = GetByLabel(LOADING_LABEL_PATH) 254 blue.synchro.SleepWallclock(250) 255 blue.synchro.Yield() 256 257 self.audio_mgr.SendUIEvent('ai_thinking_stop') 258 259 def _update_suggestions_entry(self, *args, **kwargs): 260 if self._suggestions_entry: 261 self._suggestions_entry.SetParent(self._message_scroll) 262 self._scroll_to_bottom() 263 else: 264 self._suggestions_entry = AuraSuggestionsEntry(name='suggestions', parent=self._message_scroll, align=uiconst.Align.TOTOP) 265 self._suggestions_entry.OnMouseEnter = None 266 self._suggestions_entry.OnMouseExit = None 267 268 def _add_message(self, sender_type): 269 entry = ChatMessageEntry(parent=self._message_scroll, align=uiconst.TOTOP, sender_type=sender_type, padTop=4 if len(self._message_entries) > 0 else 0) 270 self._message_entries.append(entry) 271 entry.opacity = 0 272 animations.FadeIn(entry, duration=0.3, endVal=1.0) 273 return entry 274 275 def _scroll_to_bottom(self): 276 if not self.destroyed and self._message_scroll: 277 self._message_scroll.ScrollToVertical(1.0) 278 279 def send_message(self, text): 280 self._input_field.SetValue(text) 281 self._on_send_message() 282 283 def copy_all(self): 284 t = '' 285 for entry in self._message_entries: 286 if not entry.destroyed: 287 continue 288 t += entry.get_formatted_text() + '\n' 289 290 blue.pyos.SetClipboardData(t) 291 292 def clear_chat(self): 293 for entry in self._message_entries: 294 if not entry.destroyed: 295 entry.Close() 296 297 self._message_entries = [] 298 if self._suggestions_entry: 299 self._suggestions_entry.Close() 300 self._show_welcome_state() 301 302 def connect_signals(self): 303 aura_guidance_signals.on_availability_changed.connect(self.on_availability_changed) 304 305 def disconnect_signals(self): 306 aura_guidance_signals.on_availability_changed.disconnect(self.on_availability_changed) 307 308 def on_availability_changed(self, is_feature_available): 309 if is_feature_available: 310 uicore.registry.SetFocus(self._input_field) 311 self._input_field.Enable() 312 else: 313 self._input_field.Disable() 314 if not self._welcome_container.display: 315 self.clear_chat() 316 self._show_welcome_state() 317 self._welcome_text.text = GetByLabel(NO_ASSESS_LABEL_PATH) 318 self.caption = '' 319 if self._welcome_icon is not None: 320 self._welcome_icon.display = False 321 322 def GetMenu(self): 323 menu = super(AuraGuidanceWindow, self).GetMenu() 324 if self._message_entries: 325 menu.append((MenuLabel('/Carbon/UI/Controls/Common/CopyAll'), self.copy_all)) 326 return menu 327 328 def hide_intro_conversation(self): 329 if self.conversation_svc.get_active_conversation_id() == self.intro_conversation_id: 330 self.conversation_svc.hide_conversation() 331 sm.GetService('audio').SendUIEvent(self.intro_conversation_audio_stop) 332 333 def Close(self, *args, **kwargs): 334 self.hide_intro_conversation() 335 self.disconnect_signals() 336 self.clear_chat() 337 super(AuraGuidanceWindow, self).Close(*args, **kwargs) 338 339 def Minimize(self, animate = True): 340 super(AuraGuidanceWindow, self).Minimize(animate=animate) 341 if self.IsMinimized(): 342 self.hide_intro_conversation()