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.
- neuro_simulator/agent/llm.py +23 -19
- neuro_simulator/chatbot/core.py +10 -10
- neuro_simulator/chatbot/llm.py +22 -19
- neuro_simulator/chatbot/nickname_gen/generator.py +3 -3
- neuro_simulator/chatbot/tools/manager.py +10 -8
- neuro_simulator/cli.py +7 -12
- neuro_simulator/client/assets/index-C_kzLmQy.css +1 -0
- neuro_simulator/client/assets/index-DRLWJPZv.js +7 -0
- neuro_simulator/client/assets/inter-cyrillic-400-normal-BLGc9T1a.woff2 +0 -0
- neuro_simulator/client/assets/inter-cyrillic-400-normal-alAqRL36.woff +0 -0
- neuro_simulator/client/assets/inter-cyrillic-ext-400-normal-BE2fNs0E.woff +0 -0
- neuro_simulator/client/assets/inter-cyrillic-ext-400-normal-Dc4VJyIJ.woff2 +0 -0
- neuro_simulator/client/assets/inter-greek-400-normal-C3I71FoW.woff +0 -0
- neuro_simulator/client/assets/inter-greek-400-normal-DxZsaF_h.woff2 +0 -0
- neuro_simulator/client/assets/inter-greek-ext-400-normal-Bput3-QP.woff2 +0 -0
- neuro_simulator/client/assets/inter-greek-ext-400-normal-XIH6-K3k.woff +0 -0
- neuro_simulator/client/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- neuro_simulator/client/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- neuro_simulator/client/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- neuro_simulator/client/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- neuro_simulator/client/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- neuro_simulator/client/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- neuro_simulator/client/avatar.webp +0 -0
- neuro_simulator/client/background.webp +0 -0
- neuro_simulator/client/background_old.webp +0 -0
- neuro_simulator/client/banner.jpeg +0 -0
- neuro_simulator/client/channel_points.png +0 -0
- neuro_simulator/client/error.mp3 +0 -0
- neuro_simulator/client/favicon.ico +0 -0
- neuro_simulator/client/fonts/causten.woff2 +0 -0
- neuro_simulator/client/fonts/comic.woff2 +0 -0
- neuro_simulator/client/fonts/first-coffee.woff2 +0 -0
- neuro_simulator/client/fonts/noto-sans-sc.woff2 +0 -0
- neuro_simulator/client/index.html +306 -0
- neuro_simulator/client/neuro_start.mp4 +0 -0
- neuro_simulator/client/neurosama.png +0 -0
- neuro_simulator/client/sc_pink.png +0 -0
- neuro_simulator/client/sc_purple.png +0 -0
- neuro_simulator/client/sub_badge.svg +4 -0
- neuro_simulator/client/user_avatar.jpg +0 -0
- neuro_simulator/core/agent_factory.py +9 -18
- neuro_simulator/core/application.py +86 -56
- neuro_simulator/core/config.py +88 -301
- neuro_simulator/core/path_manager.py +7 -7
- neuro_simulator/dashboard/assets/{AgentView-C6qW7TIe.js → AgentView-DBq2msN_.js} +2 -2
- neuro_simulator/dashboard/assets/{ChatBotView-BRYIM_8s.js → ChatBotView-BqQsuJUv.js} +2 -2
- neuro_simulator/dashboard/assets/ConfigView-CPYMgl_d.js +2 -0
- neuro_simulator/dashboard/assets/ConfigView-aFribfyR.css +1 -0
- neuro_simulator/dashboard/assets/{ContextTab-GRHICOS3.js → ContextTab-BSROkcd2.js} +1 -1
- neuro_simulator/dashboard/assets/{ControlView-D5vPB_OE.js → ControlView-BvflkxO-.js} +1 -1
- neuro_simulator/dashboard/assets/FieldRenderer-DyPAEyOT.js +1 -0
- neuro_simulator/dashboard/assets/LogsTab-C-SZhHdN.js +1 -0
- neuro_simulator/dashboard/assets/LogsView-82wOs2Pp.js +1 -0
- neuro_simulator/dashboard/assets/{MemoryTab-BSUWFbcV.js → MemoryTab-p3Q-Wa4e.js} +3 -3
- neuro_simulator/dashboard/assets/{ToolsTab-Bjcm3fFL.js → ToolsTab-BxbFZhXs.js} +1 -1
- neuro_simulator/dashboard/assets/index-Ba5ZG3QB.js +52 -0
- neuro_simulator/dashboard/assets/{index-C7dox9UB.css → index-CcYt9OR6.css} +1 -1
- neuro_simulator/dashboard/index.html +2 -2
- neuro_simulator/services/audio.py +55 -47
- neuro_simulator/services/builtin.py +3 -0
- neuro_simulator/services/stream.py +1 -1
- neuro_simulator/utils/queue.py +2 -2
- {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/METADATA +1 -2
- {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/RECORD +68 -35
- requirements.txt +1 -1
- neuro_simulator/config.yaml.example +0 -117
- neuro_simulator/dashboard/assets/ConfigView-Cw-VPFzt.js +0 -2
- neuro_simulator/dashboard/assets/FieldRenderer-DaTYxmtO.js +0 -1
- neuro_simulator/dashboard/assets/LogsTab-CATao-mZ.js +0 -1
- neuro_simulator/dashboard/assets/LogsView-BM419A5R.js +0 -1
- neuro_simulator/dashboard/assets/index-BiAhe8fO.js +0 -34
- neuro_simulator/services/letta.py +0 -254
- {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.5.4.dist-info → neuro_simulator-0.6.1.dist-info}/licenses/LICENSE +0 -0
neuro_simulator/agent/llm.py
CHANGED
@@ -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
|
-
|
37
|
-
|
38
|
-
if
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
48
|
-
|
49
|
-
|
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=
|
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
|
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: {
|
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."""
|
neuro_simulator/chatbot/core.py
CHANGED
@@ -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.
|
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.
|
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('
|
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.
|
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 == '
|
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.
|
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.
|
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, '
|
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.
|
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.")
|
neuro_simulator/chatbot/llm.py
CHANGED
@@ -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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
47
|
-
|
48
|
-
|
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=
|
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
|
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: {
|
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.
|
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.
|
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
|
-
"""
|
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
|
-
"
|
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.
|
65
|
-
with open(path_manager.
|
66
|
-
self.agent_tool_allocations['
|
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['
|
69
|
-
with open(path_manager.
|
70
|
-
json.dump(default_allocations['
|
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
|
52
|
-
copy_if_not_exists(package_source_path / "config.yaml
|
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
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|