neuro-simulator 0.6.0__py3-none-any.whl → 0.6.2__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.
- README.md +40 -45
- neuro_simulator/client/assets/index-BHgW05Wx.js +7 -0
- neuro_simulator/client/assets/index-rmyQl8Tr.css +1 -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/application.py +36 -1
- neuro_simulator/dashboard/assets/{AgentView-DBq2msN_.js → AgentView-DGut3feB.js} +2 -2
- neuro_simulator/dashboard/assets/{ChatBotView-BqQsuJUv.js → ChatBotView-bvwHe8hJ.js} +2 -2
- neuro_simulator/dashboard/assets/{ConfigView-CPYMgl_d.js → ConfigView-DLy2_6Tx.js} +2 -2
- neuro_simulator/dashboard/assets/{ContextTab-BSROkcd2.js → ContextTab-Cv2a7xRF.js} +1 -1
- neuro_simulator/dashboard/assets/{ControlView-BvflkxO-.js → ControlView-BVJk6r1d.js} +1 -1
- neuro_simulator/dashboard/assets/{FieldRenderer-DyPAEyOT.js → FieldRenderer-xMOXYQeU.js} +1 -1
- neuro_simulator/dashboard/assets/{LogsTab-C-SZhHdN.js → LogsTab-C7r21Vcz.js} +1 -1
- neuro_simulator/dashboard/assets/{LogsView-82wOs2Pp.js → LogsView-DF62M8uv.js} +1 -1
- neuro_simulator/dashboard/assets/{MemoryTab-p3Q-Wa4e.js → MemoryTab-BMDqRcHX.js} +1 -1
- neuro_simulator/dashboard/assets/{ToolsTab-BxbFZhXs.js → ToolsTab-Ds4M2VeQ.js} +1 -1
- neuro_simulator/dashboard/assets/{index-CcYt9OR6.css → index-D2ait7Ff.css} +2 -2
- neuro_simulator/dashboard/assets/{index-Ba5ZG3QB.js → index-Sav9Djr5.js} +3 -3
- neuro_simulator/dashboard/index.html +3 -3
- {neuro_simulator-0.6.0.dist-info → neuro_simulator-0.6.2.dist-info}/METADATA +37 -39
- {neuro_simulator-0.6.0.dist-info → neuro_simulator-0.6.2.dist-info}/RECORD +54 -20
- {neuro_simulator-0.6.0.dist-info → neuro_simulator-0.6.2.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.6.0.dist-info → neuro_simulator-0.6.2.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.6.0.dist-info → neuro_simulator-0.6.2.dist-info}/licenses/LICENSE +0 -0
README.md
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
*本临时README由AI自动生成*
|
4
4
|
|
5
|
-
这是 Neuro Simulator 的服务端,负责处理直播逻辑、AI 交互、TTS
|
5
|
+
这是 Neuro Simulator 的服务端,负责处理直播逻辑、AI 交互、TTS 合成等核心功能。
|
6
6
|
|
7
7
|
## 功能特性
|
8
8
|
|
9
|
-
-
|
9
|
+
- **动态观众**:调用LLM,基于直播内容动态生成 Chat
|
10
10
|
- **配置管理**:支持通过 API 动态修改和热重载配置
|
11
11
|
- **外部控制**:完全使用外部API端点操控服务端运行
|
12
12
|
|
@@ -15,7 +15,7 @@
|
|
15
15
|
``` main
|
16
16
|
neuro_simulator/
|
17
17
|
├── __init__.py
|
18
|
-
├── cli.py
|
18
|
+
├── cli.py
|
19
19
|
├── agent/
|
20
20
|
│ ├── __init__.py
|
21
21
|
│ ├── core.py
|
@@ -49,7 +49,6 @@ neuro_simulator/
|
|
49
49
|
│ ├── audience.py
|
50
50
|
│ ├── audio.py
|
51
51
|
│ ├── builtin.py
|
52
|
-
│ ├── letta.py
|
53
52
|
│ └── stream.py
|
54
53
|
└── utils/
|
55
54
|
├── __init__.py
|
@@ -61,11 +60,10 @@ neuro_simulator/
|
|
61
60
|
```
|
62
61
|
|
63
62
|
``` workin'dir
|
64
|
-
working_dir_example/ #
|
63
|
+
working_dir_example/ # 工作目录结构,供你参考
|
65
64
|
├── assets/ # 媒体文件夹,如缺失会使用自带资源覆盖
|
66
65
|
│ └── neuro_start.mp4 # 用来计算Start Soon长度,仅读取时长,请和客户端的视频保持一致
|
67
|
-
├── config.yaml #
|
68
|
-
├── config.yaml.example # 自动生成的配置文件模板,必须手动重命名和填写
|
66
|
+
├── config.yaml # 系统配置文件,由服务端自动管理,无需手动填写
|
69
67
|
└── agents/ # Agent相关文件夹
|
70
68
|
├── memories/ # Agent记忆文件夹
|
71
69
|
│ ├── core_memory.json
|
@@ -85,34 +83,35 @@ working_dir_example/ # 工作目录结构,请将这个目录重命名和
|
|
85
83
|
|
86
84
|
## 安装与配置
|
87
85
|
|
88
|
-
1.
|
89
|
-
- 程序会在未指定 `--dir` 的情况下自动生成一个工作目录,路径为 `~/.config/neuro-simulator
|
90
|
-
2.
|
91
|
-
3. 编辑 `config.yaml` 文件,填入必要的 API 密钥和配置项:
|
92
|
-
- 如果使用 Letta Agent,需要配置 Letta Token 和 Agent ID
|
93
|
-
- Gemini/OpenAI API Key(用于观众聊天生成和 Agent)
|
94
|
-
- Azure TTS Key 和 Region
|
86
|
+
1. 自己找一个最好是空的文件夹作为工作目录。
|
87
|
+
- 程序会在未指定 `--dir, -D` 的情况下自动生成一个工作目录,路径为 `~/.config/neuro-simulator/`。
|
88
|
+
2. 启动程序,按照外面那个 README 中的方式完成配置。
|
95
89
|
|
96
|
-
可以自行替换 `$dir/assets/neuro_start.mp4` 为其它视频文件,但记得手动替换 client
|
90
|
+
3. 可以自行替换 `$dir/assets/neuro_start.mp4` 为其它视频文件,但记得手动替换 client 中的同名文件。
|
97
91
|
|
98
92
|
### Agent配置
|
99
93
|
|
100
|
-
服务端支持两种Agent类型:
|
101
|
-
1. **Letta Agent**:需要配置 Letta Cloud 或自托管的 Letta Server
|
102
|
-
2. **内建 Agent**:使用服务端自带的 Agent,支持 Gemini 和OpenAI API
|
94
|
+
服务端支持两种 Agent 类型:
|
95
|
+
~~1. **Letta Agent**:需要配置 Letta Cloud 或自托管的 Letta Server~~ 暂时下线,后会有期。
|
96
|
+
2. **内建 Agent**:使用服务端自带的 Agent,支持 Gemini 和OpenAI API。
|
103
97
|
|
104
|
-
|
105
|
-
- `agent_type: "letta"`:使用 Letta Agent
|
106
|
-
- `agent_type: "builtin"`:使用内建 Agent
|
107
|
-
|
108
|
-
当使用内建Agent时,还需要配置:
|
109
|
-
- `agent.agent_provider`:选择"gemini"或"openai"
|
110
|
-
- `agent.agent_model`:指定具体的模型名称
|
98
|
+
不管用的是什么 Agent,在管理面板中配置和分配好服务商相关就行了。
|
111
99
|
|
112
100
|
### 直接安装方式(无需二次开发)
|
113
101
|
|
114
102
|
若无需二次开发,可以直接使用 pip 安装:
|
115
103
|
```bash
|
104
|
+
# 直接使用 pip 安装为全局软件:
|
105
|
+
pip install neuro-simulator
|
106
|
+
```
|
107
|
+
|
108
|
+
```bash
|
109
|
+
# 系统 Python 环境不宜变动时,建议使用 pipx 安装为全局软件
|
110
|
+
pipx install neuro-simulator
|
111
|
+
```
|
112
|
+
|
113
|
+
```bash
|
114
|
+
# 使用 venv 方式安装:
|
116
115
|
python3 -m venv venv
|
117
116
|
# Windows
|
118
117
|
venv/Scripts/pip install neuro-simulator
|
@@ -132,6 +131,7 @@ venv/Scripts/pip install -e .
|
|
132
131
|
# macOS/Linux
|
133
132
|
venv/bin/pip install -e .
|
134
133
|
```
|
134
|
+
安装时会自动构建 Dashboard 和 Client,请确保系统安装了 Node.js。
|
135
135
|
|
136
136
|
### 运行服务
|
137
137
|
|
@@ -142,43 +142,34 @@ neuro
|
|
142
142
|
# 指定工作目录
|
143
143
|
neuro -D /path/to/your/config
|
144
144
|
|
145
|
-
#
|
145
|
+
# 指定监听地址和端口
|
146
146
|
neuro -H 0.0.0.0 -P 8080
|
147
147
|
|
148
148
|
# 组合使用
|
149
149
|
neuro -D /path/to/your/config -H 0.0.0.0 -P 8080
|
150
150
|
```
|
151
151
|
|
152
|
-
|
152
|
+
手动指定的监听地址和端口会覆盖配置文件中的设置。
|
153
153
|
|
154
|
-
|
154
|
+
如果没有指定,服务默认遵循配置文件中的设置,运行在 `http://127.0.0.1:8000`。
|
155
155
|
|
156
|
-
|
156
|
+
## API 接口
|
157
157
|
|
158
|
-
- `/ws/admin`: 用于控制面板的管理接口,提供直播控制、配置管理、日志监控、Agent交互等所有功能,详细规范请参阅 `WEBSOCKET_API.md
|
159
|
-
- `/ws/stream`:
|
160
|
-
- `/api/system/health`:
|
161
|
-
- `/docs`: 自动生成的API文档 (Swagger UI)
|
158
|
+
- `/ws/admin`: 用于控制面板的管理接口,提供直播控制、配置管理、日志监控、Agent交互等所有功能,详细规范请参阅 `WEBSOCKET_API.md`。
|
159
|
+
- `/ws/stream`: 客户端使用的直播接口。
|
160
|
+
- `/api/system/health`: 健康检查接口。
|
162
161
|
|
163
162
|
## 配置说明
|
164
163
|
|
165
|
-
配置文件 `config.yaml`
|
166
|
-
|
167
|
-
- `api_keys` - 各种服务的 API 密钥
|
168
|
-
- `stream_metadata` - 直播元数据(标题、分类、标签等)
|
169
|
-
- `neuro_behavior` - Neuro 行为设置
|
170
|
-
- `audience_simulation` - 观众模拟设置
|
171
|
-
- `tts` - TTS 语音合成设置
|
172
|
-
- `performance` - 性能相关设置
|
173
|
-
- `server` - 服务器设置(主机、端口、CORS 等)
|
164
|
+
配置文件 `config.yaml` 现在一般无需手动编辑,所有配置项在管理面板中均可进行可视化配置。
|
174
165
|
|
175
166
|
有关配置文件的完整示例,请参阅项目根目录下的 `docs/working_dir_example/` 文件夹
|
176
167
|
|
177
168
|
## 安全说明
|
178
169
|
|
179
|
-
|
180
|
-
|
181
|
-
|
170
|
+
服务端具有 CORS 配置,仅允许预配置的来源访问。如果莫名其妙地连不上服务端(尤其是在外网环境中),可以检查和更改此项设置。
|
171
|
+
|
172
|
+
**公网部署请务必更改管理密钥**,建议更改端口为非常规端口以避免爆破。咱这个程序只是能用就行的水平,永远不要相信它的安保性能。
|
182
173
|
|
183
174
|
## 故障排除
|
184
175
|
|
@@ -186,3 +177,7 @@ neuro -D /path/to/your/config -H 0.0.0.0 -P 8080
|
|
186
177
|
- 检查网络连接是否正常
|
187
178
|
- 查看日志文件获取错误信息
|
188
179
|
- 确保端口未被其他程序占用
|
180
|
+
|
181
|
+
> 都是些 AI 生成的垃圾话,看看就好
|
182
|
+
|
183
|
+
*作为看这篇💩文档的奖励,如果你需要使用外部面板而不是服务器自托管面板,可以直接使用我部署的 https://dashboard.neuro.jiahui.cafe 连接到你的服务端,但是不保证始终能用,而且请配置好 CORS*
|
@@ -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.");
|
@@ -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(/assets/inter-cyrillic-ext-400-normal-Dc4VJyIJ.woff2) format("woff2"),url(/assets/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(/assets/inter-cyrillic-400-normal-BLGc9T1a.woff2) format("woff2"),url(/assets/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(/assets/inter-greek-ext-400-normal-Bput3-QP.woff2) format("woff2"),url(/assets/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(/assets/inter-greek-400-normal-DxZsaF_h.woff2) format("woff2"),url(/assets/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(/assets/inter-vietnamese-400-normal-DMkecbls.woff2) format("woff2"),url(/assets/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(/assets/inter-latin-ext-400-normal-C1nco2VV.woff2) format("woff2"),url(/assets/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(/assets/inter-latin-400-normal-C38fXH4l.woff2) format("woff2"),url(/assets/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}
|
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
|