NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

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