neuro-simulator 0.5.4__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. neuro_simulator/agent/llm.py +23 -19
  2. neuro_simulator/chatbot/core.py +10 -10
  3. neuro_simulator/chatbot/llm.py +22 -19
  4. neuro_simulator/chatbot/nickname_gen/generator.py +3 -3
  5. neuro_simulator/chatbot/tools/manager.py +10 -8
  6. neuro_simulator/cli.py +7 -12
  7. neuro_simulator/client/assets/index-C_kzLmQy.css +1 -0
  8. neuro_simulator/client/assets/index-DRLWJPZv.js +7 -0
  9. neuro_simulator/client/assets/inter-cyrillic-400-normal-BLGc9T1a.woff2 +0 -0
  10. neuro_simulator/client/assets/inter-cyrillic-400-normal-alAqRL36.woff +0 -0
  11. neuro_simulator/client/assets/inter-cyrillic-ext-400-normal-BE2fNs0E.woff +0 -0
  12. neuro_simulator/client/assets/inter-cyrillic-ext-400-normal-Dc4VJyIJ.woff2 +0 -0
  13. neuro_simulator/client/assets/inter-greek-400-normal-C3I71FoW.woff +0 -0
  14. neuro_simulator/client/assets/inter-greek-400-normal-DxZsaF_h.woff2 +0 -0
  15. neuro_simulator/client/assets/inter-greek-ext-400-normal-Bput3-QP.woff2 +0 -0
  16. neuro_simulator/client/assets/inter-greek-ext-400-normal-XIH6-K3k.woff +0 -0
  17. neuro_simulator/client/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  18. neuro_simulator/client/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  19. neuro_simulator/client/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  20. neuro_simulator/client/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  21. neuro_simulator/client/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  22. neuro_simulator/client/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  23. neuro_simulator/client/avatar.webp +0 -0
  24. neuro_simulator/client/background.webp +0 -0
  25. neuro_simulator/client/background_old.webp +0 -0
  26. neuro_simulator/client/banner.jpeg +0 -0
  27. neuro_simulator/client/channel_points.png +0 -0
  28. neuro_simulator/client/error.mp3 +0 -0
  29. neuro_simulator/client/favicon.ico +0 -0
  30. neuro_simulator/client/fonts/causten.woff2 +0 -0
  31. neuro_simulator/client/fonts/comic.woff2 +0 -0
  32. neuro_simulator/client/fonts/first-coffee.woff2 +0 -0
  33. neuro_simulator/client/fonts/noto-sans-sc.woff2 +0 -0
  34. neuro_simulator/client/index.html +306 -0
  35. neuro_simulator/client/neuro_start.mp4 +0 -0
  36. neuro_simulator/client/neurosama.png +0 -0
  37. neuro_simulator/client/sc_pink.png +0 -0
  38. neuro_simulator/client/sc_purple.png +0 -0
  39. neuro_simulator/client/sub_badge.svg +4 -0
  40. neuro_simulator/client/user_avatar.jpg +0 -0
  41. neuro_simulator/core/agent_factory.py +9 -18
  42. neuro_simulator/core/application.py +86 -56
  43. neuro_simulator/core/config.py +88 -301
  44. neuro_simulator/core/path_manager.py +7 -7
  45. neuro_simulator/dashboard/assets/{AgentView-C6qW7TIe.js → AgentView-DBq2msN_.js} +2 -2
  46. neuro_simulator/dashboard/assets/{ChatBotView-BRYIM_8s.js → ChatBotView-BqQsuJUv.js} +2 -2
  47. neuro_simulator/dashboard/assets/ConfigView-CPYMgl_d.js +2 -0
  48. neuro_simulator/dashboard/assets/ConfigView-aFribfyR.css +1 -0
  49. neuro_simulator/dashboard/assets/{ContextTab-GRHICOS3.js → ContextTab-BSROkcd2.js} +1 -1
  50. neuro_simulator/dashboard/assets/{ControlView-D5vPB_OE.js → ControlView-BvflkxO-.js} +1 -1
  51. neuro_simulator/dashboard/assets/FieldRenderer-DyPAEyOT.js +1 -0
  52. neuro_simulator/dashboard/assets/LogsTab-C-SZhHdN.js +1 -0
  53. neuro_simulator/dashboard/assets/LogsView-82wOs2Pp.js +1 -0
  54. neuro_simulator/dashboard/assets/{MemoryTab-BSUWFbcV.js → MemoryTab-p3Q-Wa4e.js} +3 -3
  55. neuro_simulator/dashboard/assets/{ToolsTab-Bjcm3fFL.js → ToolsTab-BxbFZhXs.js} +1 -1
  56. neuro_simulator/dashboard/assets/index-Ba5ZG3QB.js +52 -0
  57. neuro_simulator/dashboard/assets/{index-C7dox9UB.css → index-CcYt9OR6.css} +1 -1
  58. neuro_simulator/dashboard/index.html +2 -2
  59. neuro_simulator/services/audio.py +55 -47
  60. neuro_simulator/services/builtin.py +3 -0
  61. neuro_simulator/services/stream.py +1 -1
  62. neuro_simulator/utils/queue.py +2 -2
  63. {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/METADATA +1 -2
  64. {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/RECORD +68 -35
  65. requirements.txt +1 -1
  66. neuro_simulator/config.yaml.example +0 -117
  67. neuro_simulator/dashboard/assets/ConfigView-Cw-VPFzt.js +0 -2
  68. neuro_simulator/dashboard/assets/FieldRenderer-DaTYxmtO.js +0 -1
  69. neuro_simulator/dashboard/assets/LogsTab-CATao-mZ.js +0 -1
  70. neuro_simulator/dashboard/assets/LogsView-BM419A5R.js +0 -1
  71. neuro_simulator/dashboard/assets/index-BiAhe8fO.js +0 -34
  72. neuro_simulator/services/letta.py +0 -254
  73. {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/WHEEL +0 -0
  74. {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/entry_points.txt +0 -0
  75. {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -33,33 +33,37 @@ class LLMClient:
33
33
 
34
34
  logger.info("First use of built-in agent's LLMClient, performing initialization...")
35
35
  settings = config_manager.settings
36
- provider = settings.agent.agent_provider.lower()
37
-
38
- if provider == "gemini":
39
- api_key = settings.api_keys.gemini_api_key
40
- if not api_key:
41
- raise ValueError("GEMINI_API_KEY is not set in configuration for the agent.")
42
-
43
- self.client = genai.Client(api_key=api_key)
44
- self.model_name = settings.agent.agent_model
36
+
37
+ provider_id = settings.neuro.llm_provider_id
38
+ if not provider_id:
39
+ raise ValueError("LLM Provider ID is not set for the agent.")
40
+
41
+ provider_config = next((p for p in settings.llm_providers if p.provider_id == provider_id), None)
42
+ if not provider_config:
43
+ raise ValueError(f"LLM Provider with ID '{provider_id}' not found in configuration.")
44
+
45
+ provider_type = provider_config.provider_type.lower()
46
+ self.model_name = provider_config.model_name
47
+
48
+ if provider_type == "gemini":
49
+ if not provider_config.api_key:
50
+ raise ValueError(f"API key for Gemini provider '{provider_config.display_name}' is not set.")
51
+ self.client = genai.Client(api_key=provider_config.api_key)
45
52
  self._generate_func = self._generate_gemini
46
53
 
47
- elif provider == "openai":
48
- api_key = settings.api_keys.openai_api_key
49
- if not api_key:
50
- raise ValueError("OPENAI_API_KEY is not set in configuration for the agent.")
51
-
52
- self.model_name = settings.agent.agent_model
54
+ elif provider_type == "openai":
55
+ if not provider_config.api_key:
56
+ raise ValueError(f"API key for OpenAI provider '{provider_config.display_name}' is not set.")
53
57
  self.client = AsyncOpenAI(
54
- api_key=api_key,
55
- base_url=settings.api_keys.openai_api_base_url
58
+ api_key=provider_config.api_key,
59
+ base_url=provider_config.base_url
56
60
  )
57
61
  self._generate_func = self._generate_openai
58
62
  else:
59
- raise ValueError(f"Unsupported agent provider in config: {settings.agent.agent_provider}")
63
+ raise ValueError(f"Unsupported provider type in agent config: {provider_type}")
60
64
 
61
65
  self._initialized = True
62
- logger.info(f"Agent LLM client initialized. Provider: {provider.upper()}, Model: {self.model_name}")
66
+ logger.info(f"Agent LLM client initialized. Provider: {provider_type.upper()}, Model: {self.model_name}")
63
67
 
64
68
  async def _generate_gemini(self, prompt: str, max_tokens: int) -> str:
65
69
  """Generates text using the Gemini model."""
@@ -63,7 +63,7 @@ class ChatbotAgent:
63
63
  package_source_dir = Path(__file__).parent.parent
64
64
 
65
65
  files_to_copy = {
66
- "chatbot/prompts/chatbot_prompt.txt": path_manager.chatbot_agent_prompt_path,
66
+ "chatbot/prompts/chatbot_prompt.txt": path_manager.chatbot_prompt_path,
67
67
  "chatbot/prompts/memory_prompt.txt": path_manager.chatbot_memory_agent_prompt_path,
68
68
  "chatbot/memory/init_memory.json": path_manager.chatbot_init_memory_path,
69
69
  "chatbot/memory/core_memory.json": path_manager.chatbot_core_memory_path,
@@ -132,17 +132,17 @@ class ChatbotAgent:
132
132
 
133
133
  async def _build_chatbot_prompt(self, neuro_speech: str, recent_history: List[Dict[str, str]]) -> str:
134
134
  """Builds the prompt for the Chatbot (Actor) LLM."""
135
- with open(path_manager.chatbot_agent_prompt_path, 'r', encoding='utf-8') as f:
135
+ with open(path_manager.chatbot_prompt_path, 'r', encoding='utf-8') as f:
136
136
  prompt_template = f.read()
137
137
 
138
- tool_descriptions = self._format_tool_schemas_for_prompt('chatbot_agent')
138
+ tool_descriptions = self._format_tool_schemas_for_prompt('chatbot')
139
139
  init_memory_text = json.dumps(self.memory_manager.init_memory, indent=2)
140
140
  core_memory_text = json.dumps(self.memory_manager.core_memory, indent=2)
141
141
  temp_memory_text = json.dumps(self.memory_manager.temp_memory, indent=2)
142
142
  recent_history_text = "\n".join([f"{msg.get('role')}: {msg.get('content')}" for msg in recent_history])
143
143
 
144
144
  from ..core.config import config_manager
145
- chats_per_batch = config_manager.settings.chatbot_agent.chats_per_batch
145
+ chats_per_batch = config_manager.settings.chatbot.chats_per_batch
146
146
 
147
147
  return prompt_template.format(
148
148
  tool_descriptions=tool_descriptions,
@@ -186,20 +186,20 @@ class ChatbotAgent:
186
186
  params = tool_call.get("params", {})
187
187
  result = await self.tool_manager.execute_tool(tool_name, **params)
188
188
 
189
- if agent_name == 'chatbot_agent' and tool_name == "post_chat_message" and result.get("status") == "success":
189
+ if agent_name == 'chatbot' and tool_name == "post_chat_message" and result.get("status") == "success":
190
190
  text_to_post = result.get("text_to_post", "")
191
191
  if text_to_post:
192
192
  nickname = self.nickname_generator.generate_nickname()
193
193
  message = {"username": nickname, "text": text_to_post}
194
194
  generated_messages.append(message)
195
- await self._append_to_history(path_manager.chatbot_agent_history_path, {'role': 'assistant', 'content': f"{nickname}: {text_to_post}"})
195
+ await self._append_to_history(path_manager.chatbot_history_path, {'role': 'assistant', 'content': f"{nickname}: {text_to_post}"})
196
196
  logger.info(f"Returning generated messages: {generated_messages}")
197
197
  return generated_messages
198
198
 
199
199
  async def generate_chat_messages(self, neuro_speech: str, recent_history: List[Dict[str, str]]) -> List[Dict[str, str]]:
200
200
  """The main actor loop to generate chat messages."""
201
201
  for entry in recent_history:
202
- await self._append_to_history(path_manager.chatbot_agent_history_path, entry)
202
+ await self._append_to_history(path_manager.chatbot_history_path, entry)
203
203
 
204
204
  prompt = await self._build_chatbot_prompt(neuro_speech, recent_history)
205
205
  response_text = await self.chatbot_llm.generate(prompt)
@@ -208,7 +208,7 @@ class ChatbotAgent:
208
208
  tool_calls = self._parse_tool_calls(response_text)
209
209
  if not tool_calls: return []
210
210
 
211
- messages = await self._execute_tool_calls(tool_calls, 'chatbot_agent')
211
+ messages = await self._execute_tool_calls(tool_calls, 'chatbot')
212
212
 
213
213
  self.turn_counter += 1
214
214
  if self.turn_counter >= self.reflection_threshold:
@@ -220,7 +220,7 @@ class ChatbotAgent:
220
220
  """The main thinker loop to consolidate memories."""
221
221
  logger.info("Chatbot is reflecting on recent conversations...")
222
222
  self.turn_counter = 0
223
- history = await self._read_history(path_manager.chatbot_agent_history_path, limit=50)
223
+ history = await self._read_history(path_manager.chatbot_history_path, limit=50)
224
224
  if len(history) < self.reflection_threshold:
225
225
  return
226
226
 
@@ -232,4 +232,4 @@ class ChatbotAgent:
232
232
  if not tool_calls: return
233
233
 
234
234
  await self._execute_tool_calls(tool_calls, 'chatbot_memory_agent')
235
- logger.info("Chatbot memory consolidation complete.")
235
+ logger.info("Chatbot memory consolidation complete.")
@@ -31,34 +31,37 @@ class ChatbotLLMClient:
31
31
 
32
32
  logger.info("First use of Chatbot's LLMClient, performing initialization...")
33
33
  settings = config_manager.settings
34
- # Use the new chatbot_agent config block
35
- provider = settings.chatbot_agent.agent_provider.lower()
36
34
 
37
- if provider == "gemini":
38
- api_key = settings.api_keys.gemini_api_key
39
- if not api_key:
40
- raise ValueError("GEMINI_API_KEY is not set in configuration for the chatbot agent.")
41
-
42
- self.client = genai.Client(api_key=api_key)
43
- self.model_name = settings.chatbot_agent.agent_model
35
+ provider_id = settings.chatbot.llm_provider_id
36
+ if not provider_id:
37
+ raise ValueError("LLM Provider ID is not set for the chatbot.")
38
+
39
+ provider_config = next((p for p in settings.llm_providers if p.provider_id == provider_id), None)
40
+ if not provider_config:
41
+ raise ValueError(f"LLM Provider with ID '{provider_id}' not found in configuration.")
42
+
43
+ provider_type = provider_config.provider_type.lower()
44
+ self.model_name = provider_config.model_name
45
+
46
+ if provider_type == "gemini":
47
+ if not provider_config.api_key:
48
+ raise ValueError(f"API key for Gemini provider '{provider_config.display_name}' is not set.")
49
+ self.client = genai.Client(api_key=provider_config.api_key)
44
50
  self._generate_func = self._generate_gemini
45
51
 
46
- elif provider == "openai":
47
- api_key = settings.api_keys.openai_api_key
48
- if not api_key:
49
- raise ValueError("OPENAI_API_KEY is not set in configuration for the chatbot agent.")
50
-
51
- self.model_name = settings.chatbot_agent.agent_model
52
+ elif provider_type == "openai":
53
+ if not provider_config.api_key:
54
+ raise ValueError(f"API key for OpenAI provider '{provider_config.display_name}' is not set.")
52
55
  self.client = AsyncOpenAI(
53
- api_key=api_key,
54
- base_url=settings.api_keys.openai_api_base_url
56
+ api_key=provider_config.api_key,
57
+ base_url=provider_config.base_url
55
58
  )
56
59
  self._generate_func = self._generate_openai
57
60
  else:
58
- raise ValueError(f"Unsupported agent provider in chatbot_agent config: {provider}")
61
+ raise ValueError(f"Unsupported provider type in chatbot config: {provider_type}")
59
62
 
60
63
  self._initialized = True
61
- logger.info(f"Chatbot LLM client initialized. Provider: {provider.upper()}, Model: {self.model_name}")
64
+ logger.info(f"Chatbot LLM client initialized. Provider: {provider_type.upper()}, Model: {self.model_name}")
62
65
 
63
66
  async def _generate_gemini(self, prompt: str, max_tokens: int) -> str:
64
67
  """Generates text using the Gemini model."""
@@ -50,7 +50,7 @@ class NicknameGenerator:
50
50
  if not self.base_adjectives or not self.base_nouns:
51
51
  logger.warning("Base adjective or noun pools are empty. Nickname generation quality will be affected.")
52
52
 
53
- if config_manager.settings.chatbot_agent.nickname_generation.enable_dynamic_pool:
53
+ if config_manager.settings.chatbot.nickname_generation.enable_dynamic_pool:
54
54
  await self._populate_dynamic_pools()
55
55
 
56
56
  logger.info("NicknameGenerator initialized.")
@@ -58,7 +58,7 @@ class NicknameGenerator:
58
58
  async def _populate_dynamic_pools(self):
59
59
  """Uses an LLM to generate and populate the dynamic word pools."""
60
60
  logger.info("Attempting to populate dynamic nickname pools using LLM...")
61
- pool_size = config_manager.settings.chatbot_agent.nickname_generation.dynamic_pool_size
61
+ pool_size = config_manager.settings.chatbot.nickname_generation.dynamic_pool_size
62
62
  try:
63
63
  adj_prompt = f"Generate a list of {pool_size} diverse, cool-sounding English adjectives for online usernames. Output only the words, one per line."
64
64
  noun_prompt = f"Generate a list of {pool_size} diverse, cool-sounding English nouns for online usernames. Output only the words, one per line."
@@ -141,4 +141,4 @@ class NicknameGenerator:
141
141
  k=1
142
142
  )[0]
143
143
 
144
- return chosen_strategy()
144
+ return chosen_strategy()
@@ -1,5 +1,7 @@
1
1
  # neuro_simulator/chatbot/tools/manager.py
2
- """The central tool manager for the chatbot agent."""
2
+ """
3
+ The central tool manager for the chatbot agent.
4
+ """
3
5
 
4
6
  import importlib
5
7
  import inspect
@@ -56,18 +58,18 @@ class ChatbotToolManager:
56
58
  def _load_allocations(self):
57
59
  """Loads tool allocations from JSON files, creating defaults if they don't exist."""
58
60
  default_allocations = {
59
- "chatbot_agent": ["post_chat_message"],
61
+ "chatbot": ["post_chat_message"],
60
62
  "chatbot_memory_agent": ["add_temp_memory"] # Add more memory tools later
61
63
  }
62
64
 
63
65
  # Load actor agent allocations
64
- if path_manager.chatbot_agent_tools_path.exists():
65
- with open(path_manager.chatbot_agent_tools_path, 'r', encoding='utf-8') as f:
66
- self.agent_tool_allocations['chatbot_agent'] = json.load(f)
66
+ if path_manager.chatbot_tools_path.exists():
67
+ with open(path_manager.chatbot_tools_path, 'r', encoding='utf-8') as f:
68
+ self.agent_tool_allocations['chatbot'] = json.load(f)
67
69
  else:
68
- self.agent_tool_allocations['chatbot_agent'] = default_allocations['chatbot_agent']
69
- with open(path_manager.chatbot_agent_tools_path, 'w', encoding='utf-8') as f:
70
- json.dump(default_allocations['chatbot_agent'], f, indent=2)
70
+ self.agent_tool_allocations['chatbot'] = default_allocations['chatbot']
71
+ with open(path_manager.chatbot_tools_path, 'w', encoding='utf-8') as f:
72
+ json.dump(default_allocations['chatbot'], f, indent=2)
71
73
 
72
74
  # Load thinker agent allocations
73
75
  if path_manager.chatbot_memory_agent_tools_path.exists():
neuro_simulator/cli.py CHANGED
@@ -48,8 +48,8 @@ def main():
48
48
  shutil.copy(src, dest)
49
49
  logging.info(f"Copied default file to {dest}")
50
50
 
51
- # Copy config.yaml.example
52
- copy_if_not_exists(package_source_path / "config.yaml.example", work_dir / "config.yaml.example")
51
+ # Copy config.yaml if it doesn't exist
52
+ copy_if_not_exists(package_source_path / "config.yaml", work_dir / "config.yaml")
53
53
 
54
54
  # Copy prompts
55
55
  copy_if_not_exists(package_source_path / "agent" / "neuro_prompt.txt", path_manager.path_manager.neuro_prompt_path)
@@ -60,12 +60,11 @@ def main():
60
60
  copy_if_not_exists(package_source_path / "agent" / "memory" / "init_memory.json", path_manager.path_manager.init_memory_path)
61
61
  copy_if_not_exists(package_source_path / "agent" / "memory" / "temp_memory.json", path_manager.path_manager.temp_memory_path)
62
62
 
63
- # Copy default assets directory
64
- source_assets_dir = package_source_path / "assets"
65
- destination_assets_dir = path_manager.path_manager.assets_dir
66
- if not destination_assets_dir.exists():
67
- shutil.copytree(source_assets_dir, destination_assets_dir)
68
- logging.info(f"Copied default asset directory to {destination_assets_dir}")
63
+ # Copy default video asset if it doesn't exist
64
+ copy_if_not_exists(
65
+ package_source_path / "assets" / "neuro_start.mp4",
66
+ path_manager.path_manager.assets_dir / "neuro_start.mp4"
67
+ )
69
68
 
70
69
  except Exception as e:
71
70
  logging.warning(f"Could not copy all default files: {e}")
@@ -78,10 +77,6 @@ def main():
78
77
  main_config_path = path_manager.path_manager.working_dir / "config.yaml"
79
78
  try:
80
79
  config_manager.load(str(main_config_path))
81
- except FileNotFoundError:
82
- logging.error(f"FATAL: Configuration file '{main_config_path.name}' not found.")
83
- logging.error(f"If this is your first time, please rename 'config.yaml.example' to 'config.yaml' after filling it out.")
84
- sys.exit(1)
85
80
  except ValidationError as e:
86
81
  logging.error(f"FATAL: Configuration error in '{main_config_path.name}':")
87
82
  logging.error(e)
@@ -0,0 +1 @@
1
+ :root{--twitch-purple: #9147FF;--twitch-dark-bg: #F2F2F2;--twitch-chat-bg: #FFFFFF;--twitch-text-color: #18181B;--twitch-secondary-text-color: #6A6A6D;--twitch-border-color: #D3D3D3;--twitch-hover-bg: #EFEFF1;--twitch-button-bg: #EFEFF1;--twitch-button-text-color: #0E0E10;--twitch-button-hover-bg: #DFDFE1;--twitch-primary-button-bg: var(--twitch-purple);--twitch-primary-button-text-color: #FFFFFF;--twitch-primary-button-hover-bg: #772CE8;--twitch-tag-bg: #EFEFF1;--twitch-tag-text-color: #6A6A6D;--twitch-live-red: #EB0400;--twitch-viewer-red: #971311;--sidebar-width: 340px;--header-height: 50px;--neuro-shadow-color: #32003C;--neuro-avatar-display-width-percent: 50%;--fast-transition: background-color .2s, color .2s;--twitch-dark-button-bg: #333333;--twitch-dark-button-hover-bg: #444444;--twitch-dark-button-text-color: #FFFFFF;--sc-pink-bg-color: #f68795;--sc-purple-bg-color: #b97ad5}@font-face{font-family:First Coffee;src:url(../fonts/first-coffee.woff2) format("woff2");font-weight:400;font-style:normal}@font-face{font-family:Comic Sans MS;src:url(../fonts/comic.woff2) format("woff2");font-weight:400;font-style:normal}@font-face{font-family:Noto Sans SC;src:url(../fonts/noto-sans-sc.woff2) format("woff2");font-weight:400;font-style:normal}@font-face{font-family:Causten;src:url(../fonts/causten.woff2) format("woff2");font-weight:400;font-style:normal}html,body{height:100%;margin:0;padding:0;overflow:hidden}body{font-family:Inter,Arial,sans-serif;display:flex;flex-direction:column;background-color:var(--twitch-dark-bg);color:var(--twitch-text-color)}.twitch-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 10px;border-radius:100px;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:var(--twitch-button-bg);color:var(--twitch-button-text-color);height:32px;box-sizing:border-box;line-height:1}.twitch-button svg{width:1.2em;height:1.2em;fill:currentColor;vertical-align:middle}.twitch-button span{padding-top:2px}.twitch-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg)}.twitch-button.subscribe-button{background-color:var(--twitch-primary-button-bg);color:var(--twitch-primary-button-text-color)}.twitch-button.subscribe-button:hover:not(:disabled){background-color:var(--twitch-primary-button-hover-bg)}.twitch-button.icon-button{width:32px;padding:0;border-radius:50%}.nav-icon-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;border-radius:50%;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:transparent;color:var(--twitch-button-text-color);width:32px;height:32px;box-sizing:border-box;line-height:1}.nav-icon-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg);border-radius:50%}.nav-icon-button svg{width:20px;height:20px;fill:currentColor;vertical-align:middle}.search-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:transparent;color:var(--twitch-button-text-color);width:32px;height:32px;box-sizing:border-box;line-height:1}.search-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg)}.search-button svg{width:20px;height:20px;fill:currentColor;vertical-align:middle}.chat-toolbar-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;border-radius:50%;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:transparent;color:var(--twitch-button-text-color);width:32px;height:32px;box-sizing:border-box;line-height:1}.chat-toolbar-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg);border-radius:50%}.chat-toolbar-button svg{width:20px;height:20px;fill:currentColor;vertical-align:middle}.chat-toolbar-button img{width:20px;height:20px;object-fit:contain}.video-overlay-chat-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;border-radius:50%;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:#0009;color:#fff;width:32px;height:32px;box-sizing:border-box;line-height:1}.video-overlay-chat-button:hover:not(:disabled){background-color:#000c;border-radius:50%}.video-overlay-chat-button svg{width:20px;height:20px;fill:#fff;vertical-align:middle}.mute-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0;border-radius:50%;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:#0009;color:#fff;width:32px;height:32px;box-sizing:border-box;line-height:1}.mute-button:hover:not(:disabled){background-color:#000c;border-radius:50%}.mute-button svg{width:20px;height:20px;fill:#fff;vertical-align:middle}.twitch-button.dark{background-color:var(--twitch-dark-button-bg);color:var(--twitch-dark-button-text-color)}.twitch-button.dark:hover:not(:disabled){background-color:var(--twitch-dark-button-hover-bg)}.twitch-button.dark svg,.twitch-button.dark img{fill:var(--twitch-dark-button-text-color)}.twitch-button img{width:20px;height:20px;object-fit:contain;vertical-align:middle}.chat-toolbar-text-button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 10px;border-radius:100px;font-size:.9em;font-weight:600;cursor:pointer;border:none;transition:var(--fast-transition);outline:none;background-color:transparent;color:var(--twitch-button-text-color);height:32px;box-sizing:border-box;line-height:1}.chat-toolbar-text-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg)}.chat-toolbar-text-button svg,.chat-toolbar-text-button img{width:1.2em;height:1.2em;fill:currentColor;vertical-align:middle;object-fit:contain}.chat-toolbar-text-button .points-value{padding-top:2px}.chat-toolbar-text-button .channel-points-icon{width:1.2em;height:1.2em;object-fit:contain}#twitch-header{height:var(--header-height);background-color:var(--twitch-chat-bg);display:flex;justify-content:space-between;align-items:center;padding:0 1rem;border-bottom:1px solid var(--twitch-border-color);flex-shrink:0;z-index:100;color:var(--twitch-text-color);gap:1rem}.top-nav-left,.top-nav-right{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.top-nav-center{flex-grow:1;flex-shrink:1;display:flex;justify-content:center;min-width:150px}.twitch-logo-link{display:flex;align-items:center;text-decoration:none;margin-left:.5rem}.twitch-logo-container{margin:0;transform:scaleY(-1);transition:transform .3s ease-out}.twitch-logo-container:hover{transform:scaleY(1)}.twitch-logo-svg .logo-body{fill:var(--twitch-purple)}.twitch-logo-svg .logo-face{fill:var(--twitch-chat-bg)}.twitch-logo-svg .logo-eye-path{fill:var(--twitch-purple)}.nav-links-main{display:flex;gap:1.5rem;margin-left:1.5rem}.nav-link{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:var(--twitch-text-color);font-weight:500;font-size:1.1rem;padding:.25rem .5rem;border-radius:4px;transition:var(--fast-transition)}.nav-link:hover{color:var(--twitch-purple)}.nav-link p{margin:0}.search-container{display:flex;align-items:center;width:100%;max-width:400px;border:1px solid var(--twitch-border-color);border-radius:8px;overflow:hidden;background-color:var(--twitch-button-bg)}.search-input{flex:1 1 0;min-width:50px;padding:.5rem .75rem;border:none;background-color:transparent;color:var(--twitch-text-color);font-size:.95rem;outline:none}.search-input::placeholder{color:var(--twitch-secondary-text-color)}.search-button{flex-shrink:0;display:flex;align-items:center;justify-content:center;padding:0 .5rem}.search-button svg{fill:var(--twitch-text-color)}.top-nav-right{gap:.75rem}.nav-user-avatar-button{background:none;border:none;cursor:pointer;padding:0;border-radius:50%;width:32px;height:32px;overflow:hidden;flex-shrink:0}.user-avatar-wrapper{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.user-avatar-img{width:100%;height:100%;object-fit:cover;border-radius:50%}#main-content-wrapper{flex-grow:1;display:flex;overflow:hidden;position:relative}#stream-and-info-container{flex-grow:1;flex-shrink:1;display:flex;flex-direction:column;overflow-y:auto;min-height:0;scrollbar-width:none}#stream-and-info-container::-webkit-scrollbar{display:none}#stream-display-viewport{flex-grow:1;background-color:#000;display:flex;justify-content:center;align-items:center;overflow:hidden;min-height:0;position:relative}#mute-button{position:absolute;bottom:1rem;left:1rem;z-index:60}#stream-display-area{position:relative;background-color:#000;overflow:hidden;flex-shrink:0;visibility:hidden;opacity:0;transition:opacity .3s ease-in-out;container-type:size;container-name:streamArea}#background-display,#startup-video-overlay{position:absolute;top:0;left:0;width:100%;height:100%}#background-display{z-index:0;pointer-events:none;background:url(../background.webp) center/cover no-repeat}#neuro-static-avatar-container{position:absolute;left:70%;transform:translate(-50%);width:var(--neuro-avatar-display-width-percent);height:auto;overflow:visible;pointer-events:none;z-index:15;visibility:hidden;transition:transform .5s ease-in-out}#neuro-static-avatar-container img{width:100%;height:auto;display:block}.spin-animation{animation:spin-counter-clockwise .75s linear}@keyframes spin-counter-clockwise{0%{transform:translate(-50%) rotate(0)}to{transform:translate(-50%) rotate(-360deg)}}#neuro-static-avatar-container.zoom-in{transform:translate(-50%) scale(1.15)}#neuro-caption{position:absolute;bottom:5%;left:50%;transform:translate(-50%);width:90%;padding:10px 20px;border-radius:10px;text-align:center;font-weight:400;opacity:1;pointer-events:none;z-index:20;color:#fff;transition:none;text-shadow:0 0 4px var(--neuro-shadow-color),0 0 4px var(--neuro-shadow-color),0 0 4px var(--neuro-shadow-color);font-family:First Coffee,Noto Sans SC;font-size:3cqi}#neuro-caption:not(.show){opacity:0}#startup-video-overlay{background-color:#000;z-index:10;display:flex;justify-content:center;align-items:center;opacity:1;transition:none}#startup-video-overlay.hidden{opacity:0;pointer-events:none;visibility:hidden}#startup-video{width:100%;height:100%;object-fit:contain}#show-chat-button{position:absolute;top:1rem;right:1rem;z-index:60;background-color:#0009;border:none;border-radius:50%;padding:0;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;transition:background-color .2s,opacity .3s ease-in-out,visibility .3s ease-in-out;opacity:0;visibility:hidden;pointer-events:none;width:32px;height:32px;box-sizing:border-box}#show-chat-button:hover{background-color:#000c}#show-chat-button svg{width:20px;height:20px;fill:currentColor}body.chat-collapsed #show-chat-button{opacity:1;visibility:visible;pointer-events:auto}#twitch-chat-overlay{position:absolute;top:0;left:0;width:25%;height:50%;background-color:transparent;color:#fff;padding:10px;overflow-y:auto;z-index:5;font-size:1.5cqi;scrollbar-width:none;font-family:Comic Sans MS,Noto Sans SC;pointer-events:none;-webkit-user-select:none;user-select:none;scroll-behavior:auto}#twitch-chat-overlay::-webkit-scrollbar{display:none}#twitch-chat-overlay *{pointer-events:none;-webkit-user-select:none;user-select:none}#twitch-chat-overlay .chat-line__message{padding:2px 0;margin:0;text-shadow:-1px -1px 0 #00000080,1px -1px 0 #00000080,-1px 1px 0 #00000080,1px 1px 0 #00000080,2px 2px 0 rgba(0,0,0,.5);font-size:1.5cqi}#twitch-chat-overlay .chat-line__username{font-weight:700;margin-right:.25rem;font-size:1.5cqi}#twitch-chat-overlay .text-fragment{font-weight:700;color:#fff;font-size:1.5cqi}#twitch-chat-overlay .chat-author__display-name{font-size:1.5cqi}#twitch-chat-overlay .chat-line__message-container>span:not(.chat-line__username){font-weight:700;color:#fff;text-shadow:-1px -1px 0 #00000080,1px -1px 0 #00000080,-1px 1px 0 #00000080,1px 1px 0 #00000080,2px 2px 0 rgba(0,0,0,.5);font-size:1.5cqi}#stream-display-viewport *:not(.mute-button):not(#show-chat-button){pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#stream-info{padding:10px 20px;background-color:var(--twitch-chat-bg);border-bottom:1px solid var(--twitch-border-color)}.stream-info-layout{display:flex;gap:16px;align-items:center}.stream-info-left-column{flex-shrink:0}.streamer-avatar-link{display:block;position:relative}.streamer-avatar-wrapper{position:relative}#streamer-avatar{width:64px;height:64px;border-radius:50%;border:2px solid transparent;transition:border-color .2s}.streamer-avatar-link:hover #streamer-avatar{border-color:var(--twitch-purple)}.live-indicator-wrapper{position:absolute;bottom:-2px;left:50%;transform:translate(-50%);z-index:2}.live-indicator-rect{background-color:var(--twitch-live-red);color:#fff;font-size:.7em;font-weight:600;padding:2px 6px;border-radius:4px;white-space:nowrap;border:2px solid var(--twitch-chat-bg);text-transform:uppercase}.stream-info-right-column{display:flex;flex-direction:column;flex-grow:1;gap:4px;min-width:0}.stream-info-main-row{display:flex;justify-content:space-between;align-items:center}.streamer-info-and-name{display:flex;align-items:center;gap:6px;min-width:0}#streamer-nickname{margin:0;font-size:1.2em;color:var(--twitch-text-color);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.verified-badge svg{width:16px;height:16px;fill:var(--twitch-purple)}.main-action-buttons{display:flex;gap:6px;align-items:center;flex-shrink:0}#stream-info .twitch-button{display:inline-flex;align-items:center;justify-content:center;height:30px;padding:6px 10px;box-sizing:border-box}#stream-info .twitch-button.follow-button,#stream-info .twitch-button.icon-button{width:30px;padding:0}#stream-info .twitch-button:not(.follow-button):not(.icon-button){padding-top:0;padding-bottom:1px}#stream-info .twitch-button svg{width:18px;height:18px}.stream-info-details-row{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}.stream-details-left{display:flex;flex-direction:column;gap:6px;min-width:0}#stream-title-full{margin:0;font-size:.9em;font-weight:600;color:var(--twitch-text-color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stream-category-and-tags{display:flex;align-items:center;gap:6px;min-width:0}.stream-category{font-size:.9em;color:var(--twitch-purple);text-decoration:none;font-weight:500;transition:color .2s;flex-shrink:0}.stream-category:hover{text-decoration:underline}.stream-tags{display:flex;gap:6px;align-items:center;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.stream-tags::-webkit-scrollbar{display:none}.stream-tag{display:inline-flex;align-items:center;padding:3px 8px;border-radius:100px;background-color:var(--twitch-tag-bg);color:var(--twitch-tag-text-color);font-size:.75em;font-weight:600;text-decoration:none;transition:background-color .2s;height:20px;box-sizing:border-box;flex-shrink:0}.stream-tag:hover{background-color:var(--twitch-button-hover-bg)}.stream-details-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.stream-stats-section{display:flex;gap:0px;align-items:center;font-size:.9em;color:var(--twitch-secondary-text-color);justify-content:flex-end}.viewer-count,.stream-duration{display:flex;align-items:center;gap:4px}.viewer-count{color:var(--twitch-viewer-red)}.viewer-count svg{width:18px;height:18px;fill:currentColor}.viewer-count strong{font-weight:600}.stream-duration .duration-text{font-weight:500;color:var(--twitch-text-color);min-width:4.5em;text-align:right}.stream-secondary-actions{display:flex;gap:6px}.stream-secondary-actions .twitch-button{background-color:transparent}.stream-secondary-actions .twitch-button:hover:not(:disabled){background-color:var(--twitch-button-hover-bg)}#chat-sidebar{width:var(--sidebar-width);min-width:var(--sidebar-width);background-color:var(--twitch-chat-bg);flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid var(--twitch-border-color);z-index:50;height:100%;transition:width .3s ease-in-out,min-width .3s ease-in-out}#chat-sidebar.collapsed{width:0;min-width:0;overflow:hidden;visibility:hidden;pointer-events:none}.chat-sidebar-header{height:3.5rem;display:flex;justify-content:space-between;align-items:center;padding:0 1rem;border-bottom:1px solid var(--twitch-border-color);flex-shrink:0;position:relative}.chat-header-left,.chat-header-right{display:flex;align-items:center;gap:.5rem;z-index:1}.chat-sidebar-header .chat-title{position:absolute;left:50%;transform:translate(-50%);margin:0;font-size:1rem;font-weight:600;color:var(--twitch-text-color)}.chat-toggle-button{display:flex;align-items:center;justify-content:center;background:none;border:none;border-radius:50%;padding:0;cursor:pointer;color:var(--twitch-text-color);transition:background-color .2s;width:32px;height:32px;box-sizing:border-box}.chat-toggle-button:hover{background-color:var(--twitch-hover-bg)}.chat-toggle-button svg{width:20px;height:20px;fill:currentColor}#chat-sidebar.collapsed .chat-toggle-button{transform:rotate(180deg)}.messages-display{flex-grow:1;overflow-y:auto;padding:.5rem 1rem;min-height:0;scrollbar-width:thin;scrollbar-color:#B0B0B0 var(--twitch-hover-bg)}.messages-display::-webkit-scrollbar{width:8px}.messages-display::-webkit-scrollbar-track{background:var(--twitch-hover-bg);border-radius:4px}.messages-display::-webkit-scrollbar-thumb{background:#b0b0b0;border-radius:4px}.messages-display::-webkit-scrollbar-thumb:hover{background:#888}.chat-line__message{display:flex;align-items:flex-start;padding:.3rem 0;line-height:1.5;word-wrap:break-word;color:var(--twitch-text-color);border-radius:4px}.chat-line__message:hover{background-color:#ffffff0d}.chat-line__message-container{display:inline}.chat-line__username{font-weight:700;display:inline-block;margin-right:.1rem;cursor:pointer}.chat-author__display-name{font-size:.9rem}.text-fragment{font-size:.9rem;color:var(--twitch-text-color)}.chat-line__message.user-sent-message{background-color:#9147ff1a;border-left:3px solid var(--twitch-purple);padding-left:.75rem}.chat-line__message.system-message{color:var(--twitch-secondary-text-color);font-style:italic;font-size:.85rem;justify-content:center}.chat-line__message.system-message .chat-line__username,.chat-line__message.system-message .text-fragment{font-size:inherit;color:inherit;font-weight:400}.chat-input-area{padding:.75rem 1rem;border-top:1px solid var(--twitch-border-color);flex-shrink:0}.chat-input-and-buttons{display:flex;flex-direction:column;gap:.5rem}.chat-input-textarea-container{display:flex;align-items:center;border:1px solid var(--twitch-border-color);border-radius:4px;background-color:var(--twitch-button-bg);padding:0 .5rem;transition:var(--fast-transition)}.chat-input-textarea-container:focus-within{border-color:var(--twitch-purple);box-shadow:0 0 0 2px #9147ff33}.chat-input-prefix-icons,.chat-input-suffix-icons{display:flex;align-items:center}.chat-input-wrapper{flex-grow:1}.chat-input-element{width:100%;height:2.25rem;padding:0 .5rem;border:none;background-color:transparent;color:var(--twitch-text-color);font-size:.95rem;outline:none}.chat-input-element::placeholder{color:var(--twitch-secondary-text-color)}.chat-input-buttons-container{display:flex;justify-content:space-between;align-items:center}.chat-buttons-left,.chat-buttons-right{display:flex;align-items:center;gap:.5rem}.chat-points-display{display:flex;align-items:center;gap:.25rem;font-size:.85rem;color:var(--twitch-secondary-text-color)}.chat-points-display svg,.chat-points-display .channel-points-icon{width:18px;height:18px;fill:currentColor;object-fit:contain}.chat-points-display .points-value{color:var(--twitch-text-color);font-weight:600}#send-button:disabled{background-color:#b0b0b0;cursor:not-allowed}.chat-toolbar-text-button.selected{background-color:var(--twitch-purple-light);border:1px solid var(--twitch-purple);box-shadow:0 0 5px var(--twitch-purple-light)}#highlight-message-overlay{position:absolute;left:41%;transform:translate(-50%) translateY(-200%);width:24vw;height:auto;z-index:100;transition:transform 1s}#highlight-message-overlay.is-visible{transform:translate(-50%) translateY(0);transition-timing-function:cubic-bezier(.25,1,.5,1)}.sc-background-image{width:100%;height:auto;display:block;border-radius:20px}.sc-content{position:absolute;top:0;left:0;width:100%;height:100%;z-index:2;color:#fff;font-family:Causten}.sc-user{position:absolute;top:20%;left:40%;transform:translate(-50%);font-size:2cqi;text-align:left}.sc-message{position:absolute;top:60%;left:50%;transform:translate(-50%,-50%);width:90%;font-size:2cqi;text-align:center;overflow-wrap:break-word}.settings-modal-container{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1000;display:flex;justify-content:center;align-items:center}.settings-modal-container.hidden{display:none}.modal-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#0009}.modal-content{position:relative;background-color:var(--twitch-chat-bg);color:var(--twitch-text-color);border-radius:8px;width:90%;max-width:450px;box-shadow:0 4px 20px #0003;display:flex;flex-direction:column}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;border-bottom:1px solid var(--twitch-border-color)}.modal-header h2{margin:0;font-size:1.25rem}.modal-body{padding:1.5rem;display:flex;flex-direction:column;gap:1.5rem}.setting-item{display:flex;flex-direction:column;gap:.5rem}.setting-item label{font-weight:600;font-size:.9rem}.modal-input{padding:.75rem;border:1px solid var(--twitch-border-color);border-radius:4px;background-color:var(--twitch-button-bg);color:var(--twitch-text-color);font-size:1rem}.modal-input:focus{outline:none;border-color:var(--twitch-purple);box-shadow:0 0 0 2px #9147ff33}.avatar-setting .avatar-preview-container{display:flex;align-items:center;gap:1rem}.avatar-preview{width:60px;height:60px;border-radius:50%;object-fit:cover;border:2px solid var(--twitch-border-color)}.avatar-upload-input{display:none}.modal-footer{padding:1rem 1.5rem;border-top:1px solid var(--twitch-border-color);display:flex;justify-content:flex-end}.setting-description{font-size:.8rem;color:var(--twitch-secondary-text-color);margin:4px 0 0}.turbo-button-icon{display:none}.turbo-button-full{display:inline-flex}@media (max-width: 992px){.turbo-button-full{display:none}.turbo-button-icon{display:inline-flex}}@media (max-width: 767px){body,#main-content-wrapper{flex-direction:column}#stream-and-info-container{width:100%}#chat-sidebar{width:100%;height:40vh;min-height:200px;min-width:unset;border-left:none;border-top:1px solid var(--twitch-border-color)}#chat-sidebar.collapsed{height:0;min-height:0;overflow:hidden;visibility:hidden;pointer-events:none}.nav-links-main,button[aria-label=Prime],button[aria-label=打开通知],button[aria-label=悄悄话],button[aria-label=购买呼币],button[aria-label=免费体验无广告观赏],button#show-chat-button,button[aria-label=通知],button.twitch-button[aria-label=购买呼币],button.twitch-button[aria-label=赠送一次订阅],.stream-details-right,.chat-sidebar-header{display:none!important}*{scrollbar-width:none!important}*::-webkit-scrollbar{display:none!important}*::-webkit-scrollbar-thumb{display:none!important}*::-webkit-scrollbar-track{display:none!important}#streamer-avatar{width:50px;height:50px}}.tooltip{position:absolute;background:#18181b;color:#fff;padding:6px 10px;font-size:12px;border-radius:4px;pointer-events:none;opacity:0;transition:opacity .2s ease;white-space:nowrap;z-index:1000;transform-origin:center bottom;-webkit-user-select:none;user-select:none}.tooltip.show{opacity:1}.tooltip:after{content:"";position:absolute;left:50%;transform:translate(-50%);border-width:6px;border-style:solid;top:100%;border-color:#18181b transparent transparent transparent;transition:border-color .2s ease,top .2s ease}.tooltip.tooltip-arrow-up:after{top:-12px;border-color:transparent transparent #18181b transparent}.hidden{display:none!important}#offline-content-container{display:flex;flex-direction:row;align-items:center;justify-content:center;flex-wrap:wrap;width:100%;min-height:500px;flex-grow:1;background-image:url(../banner.jpeg);background-size:cover;background-position:center;background-repeat:no-repeat;padding:2rem;gap:2rem;box-sizing:border-box}.offline-info-card{display:flex;flex-direction:column;align-items:flex-start;background-color:#fff;padding:1.5rem;border-radius:0;color:#0e0e10;box-shadow:0 4px 10px #0000001a;box-sizing:border-box;flex:0 1 270px}.offline-status-section{display:flex;flex-direction:column;justify-content:center;align-items:flex-start;flex-grow:1;gap:.75rem}.offline-status-badge{background-color:#000;color:#fff;font-size:.9em;font-weight:600;padding:2px 6px;border-radius:4px;white-space:nowrap;text-transform:uppercase}.offline-status-title{font-size:1.6rem;font-weight:600;margin:0}.offline-notification-section{width:100%;margin-left:-10px}.offline-notification-button{background-color:transparent;color:var(--twitch-primary-button-hover-bg);justify-content:flex-start}.offline-notification-button:hover{background-color:var(--twitch-button-hover-bg);color:var(--twitch-button-text-color)}.offline-notification-button svg{fill:currentColor}.offline-video-player{aspect-ratio:16 / 9;flex:0 1 576px;min-width:300px;max-width:576px}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(./inter-cyrillic-ext-400-normal-Dc4VJyIJ.woff2) format("woff2"),url(./inter-cyrillic-ext-400-normal-BE2fNs0E.woff) format("woff");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(./inter-cyrillic-400-normal-BLGc9T1a.woff2) format("woff2"),url(./inter-cyrillic-400-normal-alAqRL36.woff) format("woff");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(./inter-greek-ext-400-normal-Bput3-QP.woff2) format("woff2"),url(./inter-greek-ext-400-normal-XIH6-K3k.woff) format("woff");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(./inter-greek-400-normal-DxZsaF_h.woff2) format("woff2"),url(./inter-greek-400-normal-C3I71FoW.woff) format("woff");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(./inter-vietnamese-400-normal-DMkecbls.woff2) format("woff2"),url(./inter-vietnamese-400-normal-Bbgyi5SW.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(./inter-latin-ext-400-normal-C1nco2VV.woff2) format("woff2"),url(./inter-latin-ext-400-normal-77YHD8bZ.woff) format("woff");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(./inter-latin-400-normal-C38fXH4l.woff2) format("woff2"),url(./inter-latin-400-normal-CyCys3Eg.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@@ -0,0 +1,7 @@
1
+ var U=Object.defineProperty;var N=(a,e,t)=>e in a?U(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t;var o=(a,e,t)=>N(a,typeof e!="symbol"?e+"":e,t);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))s(n);new MutationObserver(n=>{for(const i of n)if(i.type==="childList")for(const r of i.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&s(r)}).observe(document,{childList:!0,subtree:!0});function t(n){const i={};return n.integrity&&(i.integrity=n.integrity),n.referrerPolicy&&(i.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?i.credentials="include":n.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(n){if(n.ep)return;n.ep=!0;const i=t(n);fetch(n.href,i)}})();window.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll("[data-tooltip]").forEach(e=>{const t=document.createElement("div");t.className="tooltip",t.textContent=e.getAttribute("data-tooltip")||"",document.body.appendChild(t);function s(){const i=e.getBoundingClientRect(),r=window.pageXOffset,d=window.pageYOffset,h=50;let m,O;if(i.top<h)m=i.bottom+d+8,O="translateX(-50%)",t.classList.add("tooltip-arrow-up"),t.classList.remove("tooltip-arrow-down");else{t.style.left=i.left+i.width/2+r+"px",t.style.top=i.top+d+"px",t.style.transform="translateX(-50%)",t.classList.add("show");const R=t.offsetHeight;m=i.top+d-R-8,t.classList.remove("show"),t.style.top=m+"px",t.classList.add("tooltip-arrow-down"),t.classList.remove("tooltip-arrow-up")}t.style.left=i.left+i.width/2+r+"px",t.style.top=m+"px",t.style.transform=O,t.classList.add("show")}function n(){t.classList.remove("show")}e.addEventListener("mouseenter",s),e.addEventListener("mouseleave",n)})});window.addEventListener("DOMContentLoaded",()=>{const a="https://twitchtracker.com/api/channels/summary/vedal987",e=document.getElementById("avg-viewers");let t=0;const s=1;let n=null;async function i(){try{const r=new AbortController,d=setTimeout(()=>r.abort(),3e3),h=await fetch(a,{signal:r.signal});if(clearTimeout(d),!h.ok)throw new Error(`HTTP error! status: ${h.status}`);const m=await h.json();if(m&&typeof m.avg_viewers=="number")e.textContent=m.avg_viewers.toLocaleString(),t=0,n&&clearTimeout(n),n=setTimeout(i,5*60*60*1e3);else throw new Error("接口数据格式异常")}catch(r){console.warn("获取 avg_viewers 失败:",r.message),t++,t>s?(console.log("停止获取,1分钟后重试..."),setTimeout(()=>{t=0,i()},60*1e3)):i()}}i()});const p=document.getElementById("chat-messages"),y=document.getElementById("twitch-chat-overlay"),g=document.getElementById("highlight-message-overlay"),x="One_of_Swarm";class F{constructor(){p?console.log("ChatDisplay initialized."):console.error("ChatDisplay: Required chat messages container not found in DOM!")}appendChatMessage(e){if(!p){console.error("ChatDisplay: Cannot append message, container not found.");return}const t=document.createElement("div");t.className="chat-line__message";const s=document.createElement("div");s.className="chat-line__message-container",e.username===x&&e.is_user_message?t.classList.add("user-sent-message"):e.username==="System"?t.classList.add("system-message"):t.classList.add("audience-ai-message");const n=document.createElement("span");n.className="chat-line__username",n.style.color=e.username===x?"#9147FF":this.getRandomChatColor();const i=document.createElement("span");i.className="chat-author__display-name",i.textContent=e.username,n.appendChild(i);const r=document.createElement("span");r.textContent=": ",r.style.marginRight="0.3rem",r.className="text-fragment";const d=document.createElement("span");if(d.className="text-fragment",d.textContent=e.text,s.appendChild(n),s.appendChild(r),s.appendChild(d),t.appendChild(s),p.appendChild(t),y){const h=t.cloneNode(!0);y.appendChild(h),this.scrollToBottomOverlay()}this.scrollToBottom()}clearChat(){p&&(p.innerHTML="",console.log("Chat display cleared.")),y&&(y.innerHTML="")}scrollToBottom(){p&&(p.scrollTop=p.scrollHeight)}scrollToBottomOverlay(){y&&(y.scrollTop=y.scrollHeight)}getRandomChatColor(){const e=["#FF0000","#00FF00","#0000FF","#00FFFF","#FF00FF","#FF4500","#ADFF2F","#1E90FF","#FFD700","#8A2BE2","#00CED1","#FF69B4","#DA70D6","#BA55D3","#87CEEB","#32CD32","#CD853F"];return e[Math.floor(Math.random()*e.length)]}}function $(a,e,t){if(!g)return;const s=t==="bits"?"/sc_purple.png":"/sc_pink.png";g.innerHTML=`
2
+ <img src="${s}" class="sc-background-image" alt="Super Chat background">
3
+ <div class="sc-content">
4
+ <div class="sc-user">${a}</div>
5
+ <div class="sc-message">${e}</div>
6
+ </div>
7
+ `;const n=g.querySelector(".sc-message");n&&(n.style.color=t==="bits"?"var(--sc-purple-bg-color)":"var(--sc-pink-bg-color)"),g.classList.remove("hidden"),g.offsetHeight,g.classList.add("is-visible"),setTimeout(()=>{g.classList.remove("is-visible"),setTimeout(()=>{g.classList.add("hidden")},1e3)},9e3)}class W{constructor(e){o(this,"ws",null);o(this,"options");o(this,"reconnectAttempts",0);o(this,"reconnectTimeout",null);o(this,"explicitlyClosed",!1);this.options={reconnectInterval:3e3,maxReconnectAttempts:10,...e}}connect(){if(!this.options.url){console.warn("WebSocket URL is not set. Connection aborted.");return}if(this.ws&&(this.ws.readyState===WebSocket.OPEN||this.ws.readyState===WebSocket.CONNECTING)){console.warn(`WebSocket for ${this.options.url} is already connected or connecting.`);return}this.explicitlyClosed=!1,console.log(`Connecting to WebSocket: ${this.options.url}`),this.ws=new WebSocket(this.options.url),this.ws.onopen=()=>{var e,t;console.log(`WebSocket connected: ${this.options.url}`),this.reconnectAttempts=0,this.reconnectTimeout&&(clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null),(t=(e=this.options).onOpen)==null||t.call(e)},this.ws.onmessage=e=>{var t,s;try{const n=JSON.parse(e.data);n.type==="processing_superchat"&&$(n.data.username,n.data.text,n.data.sc_type),(s=(t=this.options).onMessage)==null||s.call(t,n)}catch(n){console.error(`Error parsing message from ${this.options.url}:`,n,e.data)}},this.ws.onclose=e=>{var t,s,n,i;console.warn(`WebSocket closed: ${this.options.url}. Code: ${e.code}, Reason: ${e.reason}`),this.ws=null,(s=(t=this.options).onClose)==null||s.call(t,e),this.explicitlyClosed||((i=(n=this.options).onDisconnect)==null||i.call(n),this.options.autoReconnect&&e.code!==1e3&&this.tryReconnect())},this.ws.onerror=e=>{var t,s;console.error(`WebSocket error: ${this.options.url}`,e),(s=(t=this.options).onError)==null||s.call(t,e)}}tryReconnect(){if(this.options.maxReconnectAttempts===-1||this.reconnectAttempts<this.options.maxReconnectAttempts){this.reconnectAttempts++;const t=this.options.maxReconnectAttempts===-1?`(attempt ${this.reconnectAttempts})`:`(attempt ${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`;console.log(`Attempting to reconnect to ${this.options.url} in ${this.options.reconnectInterval/1e3} seconds... ${t}`),this.reconnectTimeout=setTimeout(()=>{this.connect()},this.options.reconnectInterval)}else console.error(`Max reconnect attempts reached for ${this.options.url}.`)}send(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(JSON.stringify(e)):console.warn(`WebSocket for ${this.options.url} is not open. Message not sent.`)}disconnect(){this.explicitlyClosed=!0,this.reconnectTimeout&&clearTimeout(this.reconnectTimeout),this.ws&&this.ws.close(1e3,"Client initiated disconnect")}updateOptions(e){console.log("WebSocket client options updated:",e),this.options={...this.options,...e}}getUrl(){return this.options.url}}const c=document.getElementById("neuro-caption"),T=document.getElementById("stream-display-area"),z=16,q=2,B=.4;function M(){if(!c||!T)return;c.style.fontSize="";const a=T.offsetHeight;if(c.textContent===""||a===0)return;if(c.offsetHeight>a*B){const t=window.getComputedStyle(c);let s=parseFloat(t.fontSize);for(;c.offsetHeight>a*B&&s>z;)s-=q,c.style.fontSize=`${s}px`;c.offsetHeight>a*B&&(c.style.fontSize=`${z}px`)}}let I=null,C=null;const H=3e3;function D(a,e){if(c)if(c.classList.add("show"),e&&a.trim().length>0){const t=a.split(/\s+/).filter(r=>r.length>0);if(t.length===0){c.textContent+=(c.textContent?" ":"")+a,M();return}const s=a.length;let n=c.textContent||"";const i=r=>{if(r<t.length){const d=t[r],h=d.length/s*e*1.01,m=Math.max(50,h*1e3);n+=(n.length>0?" ":"")+d,c.textContent=n,M(),I=setTimeout(()=>i(r+1),m)}else I=null};i(0),console.log(`Starting word-by-word caption for: "${a.substring(0,30)}..." (duration: ${e}s)`)}else c.textContent+=(c.textContent?" ":"")+a,M(),console.log(`Displaying full caption: "${a.substring(0,30)}..."`)}function A(){c&&(I&&(clearTimeout(I),I=null),C&&(clearTimeout(C),C=null),c.classList.remove("show"),c.textContent="",c.style.fontSize="",console.log("NeuroCaption hidden and cleared."))}function V(){C&&clearTimeout(C),C=setTimeout(()=>{A()},H)}!c||!T?console.error("neuroCaption.ts: Could not find #neuro-caption or #stream-display-area element."):console.log("NeuroCaption module initialized.");class j{constructor(){o(this,"audioQueue",[]);o(this,"isPlayingAudio",!1);o(this,"currentPlayingAudio",null);o(this,"allSegmentsReceived",!1);o(this,"errorSound");o(this,"lastSegmentEnd",!0);this.errorSound=new Audio("/error.mp3"),console.log("AudioPlayer initialized.")}playErrorSound(){this.stopAllAudio(),console.log("Playing dedicated error sound..."),this.errorSound.play().catch(e=>{console.error("Error playing dedicated error sound:",e)})}addAudioSegment(e,t,s){this.lastSegmentEnd&&A(),this.lastSegmentEnd=!1;const n=new Audio("data:audio/mp3;base64,"+t);try{const r=f.getAppInitializer().getMuteButton();n.muted=r.getIsMuted()}catch(i){console.warn("Could not get mute state, defaulting to muted:",i),n.muted=!0}this.audioQueue.push({text:e,audio:n,duration:s}),console.log(`Audio segment added to queue. Queue size: ${this.audioQueue.length}`),this.isPlayingAudio||this.playNextAudioSegment()}playNextAudioSegment(){if(this.audioQueue.length>0&&!this.isPlayingAudio){this.isPlayingAudio=!0;const e=this.audioQueue.shift();this.currentPlayingAudio=e.audio,D(e.text,e.duration),this.currentPlayingAudio.play().catch(t=>{console.error("Error playing audio segment:",t),this.isPlayingAudio=!1,this.currentPlayingAudio=null,this.playNextAudioSegment()}),this.currentPlayingAudio.addEventListener("ended",()=>{this.isPlayingAudio=!1,this.currentPlayingAudio=null,this.playNextAudioSegment()},{once:!0})}else if(this.audioQueue.length===0&&this.allSegmentsReceived){console.log("Neuro's full audio response played. Starting caption timeout and resetting zoom."),V();try{f.getAppInitializer().getNeuroAvatar().resetZoom()}catch(e){console.warn("Could not reset neuro avatar zoom at end of speech",e)}}}setAllSegmentsReceived(){this.allSegmentsReceived=!0,this.lastSegmentEnd=!0}stopAllAudio(){this.currentPlayingAudio&&(this.currentPlayingAudio.pause(),this.currentPlayingAudio.currentTime=0,this.currentPlayingAudio=null),this.audioQueue.length=0,this.isPlayingAudio=!1,this.allSegmentsReceived=!1,A();try{f.getAppInitializer().getNeuroAvatar().resetZoom()}catch(e){console.warn("Could not reset neuro avatar zoom on stop",e)}console.log("Neuro audio playback stopped, queue cleared.")}updateMuteState(){if(this.currentPlayingAudio)try{const t=f.getAppInitializer().getMuteButton();this.currentPlayingAudio.muted=t.getIsMuted()}catch(e){console.warn("Could not update current audio mute state:",e)}for(const e of this.audioQueue)try{const s=f.getAppInitializer().getMuteButton();e.audio.muted=s.getIsMuted()}catch(t){console.warn("Could not update queued audio mute state:",t)}}}const b=document.getElementById("startup-video-overlay"),u=document.getElementById("startup-video");class Q{constructor(){!b||!u?console.error("VideoPlayer: Required video elements not found in DOM!"):console.log("VideoPlayer initialized.")}showAndPlayVideo(e=0){if(!b||!u){console.error("VideoPlayer: Cannot show and play video, elements are missing.");return}b.classList.remove("hidden"),b.style.zIndex="10";const s=f.getAppInitializer().getMuteButton(),n=()=>{isFinite(u.duration)&&e>.1&&e<u.duration&&(u.currentTime=e,console.log(`Seeked to: ${e.toFixed(2)}s.`))};u.muted=!1;const i=u.play();i!==void 0&&i.then(()=>{console.log("Unmuted autoplay successful."),n()}).catch(r=>{console.warn("Unmuted autoplay failed. Showing unmute prompt and falling back to muted playback.",r),s.show(),u.muted=!0,u.play().then(()=>{console.log("Muted fallback playback started."),n()}).catch(h=>{console.error("Muted fallback playback also failed. This is unexpected.",h)})})}pauseAndMute(){u&&(u.pause(),u.muted=!0,console.log("Startup video paused and muted."))}hide(){b&&(b.classList.add("hidden"),console.log("Startup video overlay hidden."))}getVideoDuration(){return u&&!isNaN(u.duration)?u.duration:0}}const v={HIDDEN:"hidden",STEP1:"step1",STEP2:"step2"},l=document.getElementById("neuro-static-avatar-container");class J{constructor(){l?(console.log("NeuroAvatar initialized."),this.setStage(v.HIDDEN,!0)):console.error("NeuroAvatar: Required avatar container element not found in DOM!")}startIntroAnimation(e){console.log("Starting Neuro intro animation sequence..."),this.setStage(v.STEP1),setTimeout(()=>{console.log("Animating to Step 2..."),this.setStage(v.STEP2),setTimeout(()=>{console.log("Neuro intro animation sequence finished."),e&&e()},1e3)},2e3)}setStage(e,t=!1){if(!l)return;const s="transform 0.5s ease-in-out";switch(t?(l.style.transition="none",l.offsetHeight):e===v.STEP2?l.style.transition=`bottom 1s cubic-bezier(0.4, 0.0, 1, 1), ${s}`:l.style.transition=s,e){case v.HIDDEN:l.style.visibility="hidden",l.style.bottom="-207%",l.style.left="70%",l.style.zIndex="10";break;case v.STEP1:l.style.visibility="visible",l.style.bottom="-207%",l.style.left="70%",l.style.zIndex="15";break;case v.STEP2:l.style.visibility="visible",l.style.bottom="-125%",l.style.left="70%",l.style.zIndex="15";break}}triggerSpin(){l&&(console.log("Triggering avatar spin animation."),l.classList.add("spin-animation"),setTimeout(()=>{l.classList.remove("spin-animation"),console.log("Avatar spin animation finished.")},1e3))}triggerZoom(){l&&(console.log("Triggering avatar zoom-in animation."),l.classList.add("zoom-in"))}resetZoom(){l&&l.classList.contains("zoom-in")&&(console.log("Resetting avatar zoom."),l.classList.remove("zoom-in"))}}const k=document.getElementById("chat-input"),P=document.getElementById("send-button"),w=document.getElementById("sc-bits-button"),E=document.getElementById("sc-points-button");class X{constructor(){o(this,"onSendMessageCallback",null);!k||!P||!w||!E?console.error("UserInput: Required input elements not found in DOM!"):(this.setupEventListeners(),console.log("UserInput initialized."))}onSendMessage(e){this.onSendMessageCallback=e}setupEventListeners(){P.addEventListener("click",()=>this.handleSendMessage()),k.addEventListener("keypress",e=>{e.key==="Enter"&&this.handleSendMessage()}),w.addEventListener("click",()=>this.toggleSuperchatButton(w)),E.addEventListener("click",()=>this.toggleSuperchatButton(E))}toggleSuperchatButton(e){const t=e===w?E:w;e.classList.contains("selected")?e.classList.remove("selected"):(e.classList.add("selected"),t.classList.remove("selected"))}handleSendMessage(){const e=k.value.trim();if(!e){console.warn("Attempted to send empty message.");return}let t;const s=w.classList.contains("selected"),n=E.classList.contains("selected");s||n?t={type:"superchat",text:e,sc_type:s?"bits":"points"}:t={type:"user_message",text:e},this.onSendMessageCallback?this.onSendMessageCallback(t):console.warn("No callback registered for sending message."),k.value="",this.clearSuperchatSelection()}clearSuperchatSelection(){w.classList.remove("selected"),E.classList.remove("selected")}setInputDisabled(e){k.disabled=e,P.disabled=e,console.log(`User input elements disabled: ${e}`)}}class Z{constructor(){o(this,"viewportElement");o(this,"areaElement");o(this,"resizeObserver");if(this.viewportElement=document.getElementById("stream-display-viewport"),this.areaElement=document.getElementById("stream-display-area"),!this.viewportElement||!this.areaElement)throw new Error("LayoutManager: Required viewport or area element not found in DOM!");this.resizeObserver=new ResizeObserver(this.handleResize.bind(this))}handleResize(e){for(const t of e){const{width:s,height:n}=t.contentRect;this.updateLayout(s,n)}}updateLayout(e,t){if(e===0||t===0)return;const s=e/t,n=16/9;let i,r;s>n?(r=t,i=r*n):(i=e,r=i/n),this.areaElement.style.width=`${i}px`,this.areaElement.style.height=`${r}px`}start(){this.resizeObserver.observe(this.viewportElement),this.updateLayout(this.viewportElement.clientWidth,this.viewportElement.clientHeight),console.log("LayoutManager started and observing.")}stop(){this.resizeObserver.disconnect(),console.log("LayoutManager stopped.")}}class G{constructor(){o(this,"timerElement");o(this,"intervalId",null);o(this,"streamStartTime",0);if(this.timerElement=document.getElementById("stream-duration-text"),!this.timerElement)throw new Error("StreamTimer: Duration element '#stream-duration-text' not found!");this.reset()}formatTime(e){const t=Math.floor(e/3600),s=Math.floor(e%3600/60),n=Math.floor(e%60),i=r=>String(r).padStart(2,"0");return t>0?`${t}:${i(s)}:${i(n)}`:`${s}:${i(n)}`}updateDisplay(){if(this.streamStartTime>0){const t=(Date.now()-this.streamStartTime)/1e3;this.timerElement.textContent=this.formatTime(Math.max(0,t))}}start(e=0){this.stop(),this.streamStartTime=Date.now()-e*1e3,this.updateDisplay(),this.intervalId=window.setInterval(()=>this.updateDisplay(),1e3),console.log(`Stream timer started with initial ${e.toFixed(2)}s.`)}stop(){this.intervalId!==null&&(clearInterval(this.intervalId),this.intervalId=null,console.log("Stream timer stopped."))}reset(){this.stop(),this.streamStartTime=0,this.timerElement.textContent="0:00",console.log("Stream timer reset.")}}class Y{constructor(){o(this,"sidebarElement");o(this,"toggleButton");o(this,"showChatButton");o(this,"isCollapsed",!1);o(this,"bodyElement");if(this.sidebarElement=document.getElementById("chat-sidebar"),this.toggleButton=document.getElementById("toggle-chat-button"),this.showChatButton=document.getElementById("show-chat-button"),this.bodyElement=document.body,!this.sidebarElement||!this.toggleButton||!this.showChatButton)throw new Error("ChatSidebar: Required elements not found in DOM!");this.setupEventListeners(),this.setCollapsed(!1,!0),console.log("ChatSidebar initialized.")}setupEventListeners(){this.toggleButton.addEventListener("click",()=>this.toggleCollapse()),this.showChatButton.addEventListener("click",()=>this.toggleCollapse())}toggleCollapse(){this.setCollapsed(!this.isCollapsed)}setCollapsed(e,t=!1){this.isCollapsed=e,this.isCollapsed?this.bodyElement.classList.add("chat-collapsed"):this.bodyElement.classList.remove("chat-collapsed"),t?(this.sidebarElement.style.transition="none",this.toggleButton.style.transition="none",this.showChatButton.style.transition="none"):(this.sidebarElement.style.transition="width 0.3s ease-in-out, min-width 0.3s ease-in-out",this.toggleButton.style.transition="transform 0.3s ease-in-out, background-color 0.2s, color 0.2s",this.showChatButton.style.transition="opacity 0.3s ease-in-out, visibility 0.3s ease-in-out"),this.isCollapsed?(this.sidebarElement.classList.add("collapsed"),this.toggleButton.setAttribute("aria-label","展开聊天"),this.sidebarElement.querySelectorAll(":scope > *:not(.chat-sidebar-header)").forEach(s=>{s.style.opacity="0",s.style.pointerEvents="none"}),console.log("Chat sidebar collapsed.")):(this.sidebarElement.classList.remove("collapsed"),this.toggleButton.setAttribute("aria-label","重叠聊天"),this.sidebarElement.querySelectorAll(":scope > *:not(.chat-sidebar-header)").forEach(s=>{s.style.opacity="1",s.style.pointerEvents="auto"}),console.log("Chat sidebar expanded.")),t&&requestAnimationFrame(()=>{requestAnimationFrame(()=>{this.sidebarElement.style.transition="",this.toggleButton.style.transition="",this.showChatButton.style.transition=""})})}getIsCollapsed(){return this.isCollapsed}}class K{constructor(){o(this,"indicatorElement");if(this.indicatorElement=document.querySelector(".live-indicator-rect"),!this.indicatorElement)throw new Error("LiveIndicator: Required .live-indicator-rect element not found in DOM!");this.hide()}show(){this.indicatorElement.classList.remove("hidden")}hide(){this.indicatorElement.classList.add("hidden")}}class ee{constructor(){o(this,"nicknameElement");o(this,"titleElement");o(this,"categoryElement");o(this,"tagsContainer");if(this.nicknameElement=document.getElementById("streamer-nickname"),this.titleElement=document.getElementById("stream-title-full"),this.categoryElement=document.querySelector(".stream-category"),this.tagsContainer=document.querySelector(".stream-tags"),!this.nicknameElement||!this.titleElement||!this.categoryElement||!this.tagsContainer)throw new Error("StreamInfoDisplay: One or more required elements not found in DOM!");console.log("StreamInfoDisplay initialized.")}update(e){this.nicknameElement.textContent=e.streamer_nickname,this.titleElement.textContent=e.stream_title,this.categoryElement.textContent=e.stream_category,this.tagsContainer.innerHTML="",e.stream_tags.forEach(t=>{const s=document.createElement("a");s.href="#",s.className="stream-tag",s.textContent=t,this.tagsContainer.appendChild(s)}),console.log("Stream info display updated with new metadata.")}}class te{constructor(){o(this,"wakeLockSentinel",null);o(this,"isSupported");this.isSupported="wakeLock"in navigator,this.isSupported?console.log("WakeLockManager initialized. API is supported."):console.warn("Wake Lock API is not supported in this browser. The device may go to sleep during playback.")}async requestWakeLock(){if(!(!this.isSupported||this.wakeLockSentinel))try{this.wakeLockSentinel=await navigator.wakeLock.request("screen"),this.wakeLockSentinel.addEventListener("release",()=>{console.log("Wake Lock was released by the browser."),this.wakeLockSentinel=null}),console.log("Wake Lock is active."),document.addEventListener("visibilitychange",this.handleVisibilityChange.bind(this))}catch(e){console.error(`Failed to acquire Wake Lock: ${e.name}, ${e.message}`),this.wakeLockSentinel=null}}async releaseWakeLock(){this.wakeLockSentinel&&(await this.wakeLockSentinel.release(),this.wakeLockSentinel=null,console.log("Wake Lock has been released.")),document.removeEventListener("visibilitychange",this.handleVisibilityChange.bind(this))}async handleVisibilityChange(){this.wakeLockSentinel===null&&document.visibilityState==="visible"&&(console.log("Page is visible again, re-acquiring Wake Lock..."),await this.requestWakeLock())}}class L{constructor(e){o(this,"modalContainer");o(this,"overlay");o(this,"closeButton");o(this,"saveButton");o(this,"usernameInput");o(this,"backendUrlInput");o(this,"avatarPreview");o(this,"avatarUploadInput");o(this,"avatarUploadButton");o(this,"reconnectAttemptsInput");o(this,"onSave");if(this.onSave=e,this.modalContainer=document.getElementById("settings-modal"),this.overlay=document.getElementById("settings-modal-overlay"),this.closeButton=document.getElementById("settings-close-button"),this.saveButton=document.getElementById("settings-save-button"),this.usernameInput=document.getElementById("username-setting-input"),this.backendUrlInput=document.getElementById("backend-url-input"),this.avatarPreview=document.getElementById("avatar-setting-preview"),this.avatarUploadInput=document.getElementById("avatar-setting-upload"),this.avatarUploadButton=document.getElementById("avatar-upload-button"),this.reconnectAttemptsInput=document.getElementById("reconnect-attempts-input"),!this.modalContainer)throw new Error("Settings modal container not found!");this.setupEventListeners(),console.log("SettingsModal initialized.")}setupEventListeners(){this.closeButton.addEventListener("click",()=>this.close()),this.overlay.addEventListener("click",()=>this.close()),this.saveButton.addEventListener("click",()=>this.handleSave()),this.avatarUploadButton.addEventListener("click",()=>this.avatarUploadInput.click()),this.avatarUploadInput.addEventListener("change",e=>this.handleAvatarUpload(e))}handleAvatarUpload(e){const t=e.target;if(t.files&&t.files[0]){const s=new FileReader;s.onload=n=>{var i;(i=n.target)!=null&&i.result&&(this.avatarPreview.src=n.target.result)},s.readAsDataURL(t.files[0])}}handleSave(){const e={username:this.usernameInput.value.trim()||"User",avatarDataUrl:this.avatarPreview.src,backendUrl:this.backendUrlInput.value.trim(),reconnectAttempts:parseInt(this.reconnectAttemptsInput.value,10)||-1};localStorage.setItem("neuro_settings",JSON.stringify(e)),this.onSave(e),this.close()}open(){this.loadSettings(),this.modalContainer.classList.remove("hidden")}close(){this.modalContainer.classList.add("hidden")}loadSettings(){const e=L.getSettings();this.usernameInput.value=e.username,this.avatarPreview.src=e.avatarDataUrl,this.backendUrlInput.value=e.backendUrl,this.reconnectAttemptsInput.value=String(e.reconnectAttempts)}static getSettings(){const e=localStorage.getItem("neuro_settings");if(e)try{return JSON.parse(e)}catch(t){console.error("Failed to parse settings from localStorage, returning defaults.",t)}return{username:"One_of_Swarm",avatarDataUrl:"/user_avatar.jpg",backendUrl:"ws://127.0.0.1:8000",reconnectAttempts:-1}}}class se{constructor(){o(this,"button",null);o(this,"isMuted",!0)}create(){return this.button=document.getElementById("mute-button"),this.button?this.button.addEventListener("click",e=>{e.stopPropagation(),this.unmute()}):console.error("Mute button element not found in DOM!"),this.button}show(){this.button&&(this.button.style.display="flex")}hide(){this.button&&(this.button.style.display="none")}unmute(){this.isMuted=!1,this.hide(),this.updateMediaElements()}updateMediaElements(){const e=document.getElementById("startup-video");e&&(e.muted=this.isMuted);try{f.getAppInitializer().getAudioPlayer().updateMuteState()}catch(t){console.warn("Could not update audio player mute state:",t)}}getElement(){return this.button}getIsMuted(){return this.isMuted}}async function ne(a,e={},t){return window.__TAURI_INTERNALS__.invoke(a,e,t)}const ie=typeof window<"u"&&window.__TAURI__!==void 0,oe="3546729368520811",ae="4281748";async function re(){var a,e;try{if(ie)return console.log("Running in Tauri, invoking 'get_latest_replay_video' command..."),await ne("get_latest_replay_video");{console.log("Running in Web, fetching from proxy...");const t=`/bilibili-api/x/series/archives?mid=${oe}&series_id=${ae}&ps=1&pn=1`,s=await fetch(t);if(!s.ok)throw new Error(`Failed to fetch from proxy: ${s.statusText}`);const n=await s.json();if(n.code!==0)throw new Error(`Bilibili API error: ${n.message}`);const i=(e=(a=n==null?void 0:n.data)==null?void 0:a.archives)==null?void 0:e[0];if(i&&i.bvid&&i.aid)return i;throw new Error("No matching video found in API response.")}}catch(t){return console.error("Failed to get latest replay video:",t),null}}function le(a){const e="//www.bilibili.com/blackboard/html5mobileplayer.html",t=new URLSearchParams({bvid:a.bvid,aid:String(a.aid),p:"1",autoplay:"1",danmaku:"0",hasMuteButton:"1",hideCoverInfo:"0",fjw:"0",high_quality:"1"});return`${e}?${t.toString()}`}class ce{constructor(){o(this,"wsClient");o(this,"audioPlayer");o(this,"videoPlayer");o(this,"neuroAvatar");o(this,"chatDisplay");o(this,"userInput");o(this,"layoutManager");o(this,"streamTimer");o(this,"chatSidebar");o(this,"liveIndicator");o(this,"streamInfoDisplay");o(this,"wakeLockManager");o(this,"settingsModal");o(this,"currentSettings");o(this,"muteButton");o(this,"resizeObserver",null);o(this,"offlinePlayerSrc",null);o(this,"isStarted",!1);o(this,"currentPhase","offline");o(this,"adjustOfflineLayout",()=>{if(this.currentPhase!=="offline")return;const e=document.getElementById("offline-content-container"),t=document.querySelector(".offline-video-player"),s=document.querySelector(".offline-info-card");if(!e||!t||!s)return;if(window.innerWidth<=767)e.style.flexWrap="wrap",s.style.width=t.offsetWidth+"px",s.style.height="auto",s.style.flex="0 0 auto";else{e.style.flexWrap="nowrap";const i=t.offsetHeight;i>0&&(s.style.height=`${i}px`,s.style.width=`${i}px`),s.style.flex="0 1 auto"}});this.layoutManager=new Z,this.streamTimer=new G,this.muteButton=new se,this.currentSettings=L.getSettings(),this.settingsModal=new L(t=>this.handleSettingsUpdate(t)),this.wsClient=null,this.audioPlayer=new j,this.videoPlayer=new Q,this.neuroAvatar=new J,this.chatDisplay=new F,this.userInput=new X,this.userInput.onSendMessage(t=>this.sendUserMessage(t)),this.chatSidebar=new Y,this.liveIndicator=new K,this.streamInfoDisplay=new ee,this.wakeLockManager=new te,this.setupSettingsModalTrigger(),this.setupMuteButton();const e=document.querySelector(".offline-video-player");e&&(this.offlinePlayerSrc=e.src),this.updateOfflinePlayerSrc()}start(){this.isStarted||(this.isStarted=!0,this.probeForIntegratedServer(),this.layoutManager.start(),this.goOffline(),this.updateUiWithSettings())}setupSettingsModalTrigger(){const e=document.querySelector(".nav-user-avatar-button");e&&e.addEventListener("click",()=>{this.settingsModal.open()})}setupMuteButton(){if(this.muteButton.create()){this.muteButton.show();const t=()=>{this.muteButton.unmute(),document.removeEventListener("click",t)};document.addEventListener("click",t)}}getMuteButton(){return this.muteButton}getAudioPlayer(){return this.audioPlayer}getNeuroAvatar(){return this.neuroAvatar}handleSettingsUpdate(e){if(console.log("Settings updated. Re-initializing connection with new settings:",e),this.currentSettings=e,this.updateUiWithSettings(),this.wsClient){const t=e.backendUrl?`${e.backendUrl}/ws/stream`:"";this.wsClient.updateOptions({url:t,maxReconnectAttempts:e.reconnectAttempts}),this.wsClient.disconnect(),setTimeout(()=>{this.wsClient&&this.wsClient.getUrl()?this.wsClient.connect():console.warn("Cannot connect: Backend URL is empty after update or WebSocket client not ready.")},500)}else console.warn("WebSocket client not initialized, cannot update settings.")}initWebSocketClient(e){const t=e?`${e}/ws/stream`:"",s=n=>this.handleWebSocketMessage(n);this.wsClient?(this.wsClient.updateOptions({url:t,autoReconnect:!0,maxReconnectAttempts:this.currentSettings.reconnectAttempts,onMessage:s,onOpen:()=>this.goOnline(),onDisconnect:()=>this.goOffline()}),t&&(this.wsClient.disconnect(),setTimeout(()=>{this.wsClient.connect()},500))):this.wsClient=new W({url:t,autoReconnect:!0,maxReconnectAttempts:this.currentSettings.reconnectAttempts,onMessage:s,onOpen:()=>this.goOnline(),onDisconnect:()=>this.goOffline()})}async probeForIntegratedServer(){const e=L.getSettings();console.log("Probing for integrated server. Stored settings:",e);try{const t=new URL("/api/system/health",window.location.origin).toString();console.log("Probing integrated server at:",t);const s=await fetch(t);if(s.ok){console.log("Integrated server detected via health check. Auto-connecting...");const n=window.location.origin;this.currentSettings={...this.currentSettings,backendUrl:n},localStorage.setItem("neuro_settings",JSON.stringify(this.currentSettings)),this.initWebSocketClient(n),this.wsClient&&n&&this.wsClient.connect();return}else console.log("Health check failed, not an integrated server. Status:",s.status)}catch(t){console.log("Failed to probe for integrated server, assuming standalone mode.",t)}console.log("Falling back to stored backend URL:",e.backendUrl),this.initWebSocketClient(e.backendUrl),this.wsClient&&e.backendUrl?this.wsClient.connect():e.backendUrl||(console.warn("Backend URL is not configured via probe or stored settings. Opening settings modal."),this.settingsModal.open())}updateUiWithSettings(){document.querySelectorAll(".user-avatar-img").forEach(t=>t.src=this.currentSettings.avatarDataUrl),console.log(`UI updated with username: ${this.currentSettings.username} and avatar.`)}async updateOfflinePlayerSrc(){console.log("Attempting to fetch the latest replay video...");const e=await re();if(e){const t=le(e);if(this.offlinePlayerSrc=t,console.log("Successfully updated offline player src to:",t),this.currentPhase==="offline"){const s=document.querySelector(".offline-video-player");s&&(s.src=this.offlinePlayerSrc)}}else console.log("Failed to fetch latest replay video, using default fallback.")}goOnline(){var s,n,i,r;console.log("Entering ONLINE state."),this.updateUiWithSettings();const e=document.querySelector(".offline-video-player");e&&(e.src="about:blank"),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),window.removeEventListener("resize",this.adjustOfflineLayout);const t=document.querySelector(".offline-info-card");t&&(t.style.height="",t.style.width=""),(s=document.getElementById("offline-content-container"))==null||s.classList.add("hidden"),(n=document.getElementById("stream-display-viewport"))==null||n.classList.remove("hidden"),(i=document.querySelector(".stream-info-details-row"))==null||i.classList.remove("hidden"),(r=document.getElementById("chat-sidebar"))==null||r.classList.remove("hidden"),this.showStreamContent(),this.chatDisplay.clearChat(),this.liveIndicator.show(),this.wakeLockManager.requestWakeLock()}goOffline(){var s,n,i,r;console.log("Entering OFFLINE state."),this.currentPhase="offline";const e=document.querySelector(".offline-video-player");e&&this.offlinePlayerSrc&&(e.src=this.offlinePlayerSrc),(s=document.getElementById("offline-content-container"))==null||s.classList.remove("hidden"),(n=document.getElementById("stream-display-viewport"))==null||n.classList.add("hidden"),(i=document.querySelector(".stream-info-details-row"))==null||i.classList.add("hidden"),(r=document.getElementById("chat-sidebar"))==null||r.classList.add("hidden"),this.hideStreamContent(),this.audioPlayer.stopAllAudio(),this.videoPlayer.hide(),this.neuroAvatar.setStage("hidden",!0),A(),this.streamTimer.stop(),this.streamTimer.reset(),this.chatDisplay.clearChat(),this.liveIndicator.hide(),this.wakeLockManager.releaseWakeLock(),this.muteButton.show();const t=()=>{this.muteButton.unmute(),document.removeEventListener("click",t)};document.addEventListener("click",t),setTimeout(()=>{this.adjustOfflineLayout();const d=document.querySelector(".offline-video-player");d&&!this.resizeObserver&&(this.resizeObserver=new ResizeObserver(this.adjustOfflineLayout),this.resizeObserver.observe(d),window.addEventListener("resize",this.adjustOfflineLayout))},0)}handleWebSocketMessage(e){switch(this.currentPhase==="offline"&&["play_welcome_video","start_avatar_intro","enter_live_phase"].includes(e.type)&&(console.log("Connection successful, transitioning from OFFLINE to active state."),this.goOnline()),e.elapsed_time_sec!==void 0&&this.streamTimer.start(e.elapsed_time_sec),e.type){case"offline":this.goOffline();break;case"model_spin":this.neuroAvatar.triggerSpin();break;case"model_zoom":this.neuroAvatar.triggerZoom();break;case"update_stream_metadata":this.streamInfoDisplay.update(e);break;case"play_welcome_video":this.currentPhase="initializing",this.videoPlayer.showAndPlayVideo(parseFloat(e.progress));break;case"start_avatar_intro":this.currentPhase="avatar_intro",this.videoPlayer.pauseAndMute(),this.neuroAvatar.startIntroAnimation(()=>{this.videoPlayer.hide()});break;case"enter_live_phase":this.currentPhase="live",this.videoPlayer.hide(),this.neuroAvatar.setStage("step2");break;case"neuro_is_speaking":break;case"neuro_speech_segment":const t=e;t.is_end?this.audioPlayer.setAllSegmentsReceived():t.audio_base64&&t.text&&typeof t.duration=="number"?this.audioPlayer.addAudioSegment(t.text,t.audio_base64,t.duration):console.warn("Received neuro_speech_segment message with missing audio/text/duration:",t);break;case"neuro_error_signal":console.warn("Received neuro_error_signal from backend."),D("Someone tell Vedal there is a problem with my AI."),this.audioPlayer.playErrorSound();break;case"chat_message":(!this.chatSidebar.getIsCollapsed()||e.is_user_message)&&this.chatDisplay.appendChatMessage(e);break;case"error":this.chatDisplay.appendChatMessage({type:"chat_message",username:"System",text:`后端错误: ${e.message}`,is_user_message:!1});break}}sendUserMessage(e){const t={username:this.currentSettings.username,...e};this.wsClient?this.wsClient.send(t):console.warn("Cannot send message: WebSocket client is not initialized.")}showStreamContent(){const e=document.getElementById("stream-display-area");e&&(e.style.visibility="visible",e.style.opacity="1")}hideStreamContent(){const e=document.getElementById("stream-display-area");e&&(e.style.visibility="hidden",e.style.opacity="0")}}const S=class S{constructor(){o(this,"appInitializerInstance",null)}static getInstance(){return S.instance||(S.instance=new S),S.instance}getAppInitializer(){return this.appInitializerInstance?console.log("Returning existing AppInitializer instance."):(console.log("Creating new AppInitializer instance..."),this.appInitializerInstance=new ce),this.appInitializerInstance}};o(S,"instance");let _=S;const f=_.getInstance();document.addEventListener("DOMContentLoaded",()=>{console.log("DOMContentLoaded event fired."),f.getAppInitializer().start()});console.log("main.ts loaded. Waiting for DOMContentLoaded to initialize the app.");
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file