@liguoshuai/pi-web-chat 2.17.8 → 2.19.1
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.
- package/README.md +2 -2
- package/package.json +4 -4
- package/public/app.js +44 -14
- package/public/index.html +2 -3
- package/public/style.css +7 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@liguoshuai/pi-web-chat)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
|
|
7
7
|
一个 [pi](https://pi.dev) 编程代理的 Web 界面,风格参考 ChatGPT / Gemini ——
|
|
8
8
|
左侧历史会话侧边栏 + 右侧对话区 + 底部输入框。底层通过 pi 的 **RPC 模式**
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
## 🚀 快速开始
|
|
36
36
|
|
|
37
37
|
### 1. 环境准备
|
|
38
|
-
- **Node.js**: `>=
|
|
38
|
+
- **Node.js**: `>= 20.0.0`
|
|
39
39
|
- **操作系统**: Linux / macOS / WSL (Windows)
|
|
40
40
|
|
|
41
41
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liguoshuai/pi-web-chat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"description": "A ChatGPT/Gemini-style web UI for the pi coding agent, powered by pi's RPC mode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
40
|
+
"node": ">=20"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"express": "^4.21.2",
|
|
44
44
|
"ws": "^8.18.0",
|
|
45
|
-
"@liguoshuai/pi-chat-protocol": "2.
|
|
46
|
-
"@liguoshuai/pi-chat-server": "2.
|
|
45
|
+
"@liguoshuai/pi-chat-protocol": "2.19.1",
|
|
46
|
+
"@liguoshuai/pi-chat-server": "2.19.1"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "node --check server.js && node --check bin/pi-web-chat.js && node --check public/markdown.js && node --check public/app.js",
|
package/public/app.js
CHANGED
|
@@ -168,8 +168,17 @@ async function loadServerConfig() {
|
|
|
168
168
|
}
|
|
169
169
|
if (data.version) {
|
|
170
170
|
state.version = data.version;
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
}
|
|
172
|
+
if (data.piVersion) {
|
|
173
|
+
state.piVersion = data.piVersion;
|
|
174
|
+
}
|
|
175
|
+
const piVerEl = $("#piVersion");
|
|
176
|
+
if (piVerEl) {
|
|
177
|
+
piVerEl.textContent = state.piVersion ? ` v${state.piVersion}` : "";
|
|
178
|
+
}
|
|
179
|
+
const verEl = $("#appVersion");
|
|
180
|
+
if (verEl) {
|
|
181
|
+
verEl.textContent = state.version ? ` v${state.version}` : "";
|
|
173
182
|
}
|
|
174
183
|
if (data.defaultModel) {
|
|
175
184
|
state.defaultModel = data.defaultModel;
|
|
@@ -372,13 +381,7 @@ function startStreamingTimer() {
|
|
|
372
381
|
el.style.display = "";
|
|
373
382
|
}
|
|
374
383
|
});
|
|
375
|
-
|
|
376
|
-
liveTools.forEach((el) => {
|
|
377
|
-
if (el._startedAt) {
|
|
378
|
-
el.textContent = formatDuration(now - el._startedAt);
|
|
379
|
-
el.style.display = "";
|
|
380
|
-
}
|
|
381
|
-
});
|
|
384
|
+
|
|
382
385
|
// Live update for the thinking placeholder spinner label before first delta arrives
|
|
383
386
|
const placeholder = document.querySelector(".thinking-placeholder");
|
|
384
387
|
if (placeholder && state.turnStartedAt) {
|
|
@@ -393,7 +396,7 @@ function startStreamingTimer() {
|
|
|
393
396
|
}
|
|
394
397
|
}
|
|
395
398
|
}
|
|
396
|
-
},
|
|
399
|
+
}, 200);
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
function stopStreamingTimer() {
|
|
@@ -763,6 +766,7 @@ function reconstructFromEntries(entries, timing = null) {
|
|
|
763
766
|
lastUserTs = msgTs;
|
|
764
767
|
turnIndex++;
|
|
765
768
|
thinkingIdx = 0; // reset thinking index for the new turn
|
|
769
|
+
const isSteer = Boolean(m.isSteer || e.isSteer || e.customType === "steer" || m.customType === "steer");
|
|
766
770
|
// Extract optional images from user message content array
|
|
767
771
|
let images = [];
|
|
768
772
|
if (Array.isArray(m.content)) {
|
|
@@ -774,7 +778,7 @@ function reconstructFromEntries(entries, timing = null) {
|
|
|
774
778
|
url: c.data ? `data:${c.mimeType || "image/png"};base64,${c.data}` : (c.url || "")
|
|
775
779
|
}));
|
|
776
780
|
}
|
|
777
|
-
out.push({ role: "user", text: extractContentText(m.content), images, ts: msgTs });
|
|
781
|
+
out.push({ role: "user", text: extractContentText(m.content), images, ts: msgTs, isSteer });
|
|
778
782
|
} else if (m.role === "assistant") {
|
|
779
783
|
let turnDurationMs = null;
|
|
780
784
|
// Try timing data first, then fall back to timestamp heuristic.
|
|
@@ -842,10 +846,19 @@ function extractContentText(content) {
|
|
|
842
846
|
.join("\n\n");
|
|
843
847
|
}
|
|
844
848
|
|
|
845
|
-
function appendSystemNotice(text) {
|
|
849
|
+
function appendSystemNotice(text, replaceIfLast = true) {
|
|
846
850
|
if (!text) return;
|
|
847
851
|
const chatInner = $("#chat-inner");
|
|
848
852
|
if (!chatInner) return;
|
|
853
|
+
const lastChild = chatInner.lastElementChild;
|
|
854
|
+
if (replaceIfLast && lastChild && lastChild.classList.contains("system-notice-divider")) {
|
|
855
|
+
const textEl = lastChild.querySelector(".system-notice-text");
|
|
856
|
+
if (textEl) {
|
|
857
|
+
textEl.textContent = text;
|
|
858
|
+
scrollBottom();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
849
862
|
const node = el("div", { class: "system-notice-divider" }, [
|
|
850
863
|
el("span", { class: "system-notice-text", text })
|
|
851
864
|
]);
|
|
@@ -1657,6 +1670,9 @@ function refreshStreamingContent() {
|
|
|
1657
1670
|
el("span", { class: "thinking-spinner" }),
|
|
1658
1671
|
el("span", { class: "thinking-label", text: "正在思考中…" })
|
|
1659
1672
|
]));
|
|
1673
|
+
content.appendChild(el("div", { class: "streaming-cursor-row" }, [
|
|
1674
|
+
el("span", { class: "typing-cursor" })
|
|
1675
|
+
]));
|
|
1660
1676
|
}
|
|
1661
1677
|
}
|
|
1662
1678
|
scrollBottom();
|
|
@@ -1669,7 +1685,7 @@ function refreshStreamingContent() {
|
|
|
1669
1685
|
const lastItem = items[items.length - 1];
|
|
1670
1686
|
if (lastItem.type === "text") {
|
|
1671
1687
|
const lastEl = content.lastElementChild;
|
|
1672
|
-
if (lastEl && !lastEl.classList.contains("thinking-block") && !lastEl.classList.contains("tool-block")) {
|
|
1688
|
+
if (lastEl && !lastEl.classList.contains("thinking-block") && !lastEl.classList.contains("tool-block") && !lastEl.classList.contains("streaming-cursor-row")) {
|
|
1673
1689
|
const showCursor = state.streaming;
|
|
1674
1690
|
lastEl.innerHTML = renderMarkdown(lastItem.text) + (showCursor ? '<span class="typing-cursor"></span>' : "");
|
|
1675
1691
|
scrollBottom();
|
|
@@ -1681,6 +1697,7 @@ function refreshStreamingContent() {
|
|
|
1681
1697
|
// Full rebuild (new items added or structure changed)
|
|
1682
1698
|
content.innerHTML = "";
|
|
1683
1699
|
const lastIdx = items.length - 1;
|
|
1700
|
+
let hasText = false;
|
|
1684
1701
|
for (let i = 0; i < items.length; i++) {
|
|
1685
1702
|
const item = items[i];
|
|
1686
1703
|
const isLast = (i === lastIdx);
|
|
@@ -1690,10 +1707,16 @@ function refreshStreamingContent() {
|
|
|
1690
1707
|
} else if (item.type === "tool") {
|
|
1691
1708
|
if (item.tc?.block) content.appendChild(item.tc.block);
|
|
1692
1709
|
} else if (item.type === "text") {
|
|
1710
|
+
hasText = true;
|
|
1693
1711
|
const showCursor = state.streaming && isLast;
|
|
1694
1712
|
content.appendChild(el("div", { html: renderMarkdown(item.text) + (showCursor ? '<span class="typing-cursor"></span>' : "") }));
|
|
1695
1713
|
}
|
|
1696
1714
|
}
|
|
1715
|
+
if (state.streaming && !hasText) {
|
|
1716
|
+
content.appendChild(el("div", { class: "streaming-cursor-row" }, [
|
|
1717
|
+
el("span", { class: "typing-cursor" })
|
|
1718
|
+
]));
|
|
1719
|
+
}
|
|
1697
1720
|
lastRenderedItemCount = items.length;
|
|
1698
1721
|
scrollBottom();
|
|
1699
1722
|
}
|
|
@@ -3077,6 +3100,13 @@ function submitSteer() {
|
|
|
3077
3100
|
return;
|
|
3078
3101
|
}
|
|
3079
3102
|
|
|
3103
|
+
// If there was an active streaming assistant block before steering, finalize it
|
|
3104
|
+
// so that subsequent assistant chunks render in a new block below this steer message!
|
|
3105
|
+
if (state.streamingMsg) {
|
|
3106
|
+
finalizeStreamingMsg();
|
|
3107
|
+
state.streaming = true; // resume streaming flag for subsequent response
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3080
3110
|
appendMessageNode("user", { text, isSteer: true, ts: Date.now() });
|
|
3081
3111
|
ta.value = "";
|
|
3082
3112
|
autoResize();
|
|
@@ -3144,7 +3174,7 @@ function submitPrompt() {
|
|
|
3144
3174
|
if (state.streamingWatchdog) clearTimeout(state.streamingWatchdog);
|
|
3145
3175
|
state.streamingWatchdog = setTimeout(() => {
|
|
3146
3176
|
if (state.streaming) {
|
|
3147
|
-
|
|
3177
|
+
showToast("生成超时(5 分钟无响应),已自动结束。");
|
|
3148
3178
|
finalizeStreamingMsg();
|
|
3149
3179
|
}
|
|
3150
3180
|
}, 5 * 60 * 1000);
|
package/public/index.html
CHANGED
|
@@ -30,11 +30,10 @@
|
|
|
30
30
|
<div id="connStatus" class="conn-status" title="点击可重新连接"><span id="connDot" style="color: var(--danger);">●</span> <span id="connLabel">连接中…</span></div>
|
|
31
31
|
<div class="sidebar-bottom-meta">
|
|
32
32
|
<div class="sidebar-bottom-links">
|
|
33
|
-
<a href="https://pi.dev" target="_blank" rel="noopener">pi
|
|
33
|
+
<a href="https://pi.dev" target="_blank" rel="noopener">pi<span id="piVersion"></span></a>
|
|
34
34
|
<span class="sidebar-link-sep">·</span>
|
|
35
|
-
<a href="https://github.com/liguoshuai-1990/pi-
|
|
35
|
+
<a href="https://github.com/liguoshuai-1990/pi-chat" target="_blank" rel="noopener">pi-chat<span id="appVersion"></span></a>
|
|
36
36
|
</div>
|
|
37
|
-
<span id="appVersion" class="app-version" title="pi-web-chat 版本"></span>
|
|
38
37
|
</div>
|
|
39
38
|
</div>
|
|
40
39
|
<div class="sidebar-resizer" id="sidebarResizer" title="拖拽调整侧边栏宽度,双击恢复默认"></div>
|
package/public/style.css
CHANGED
|
@@ -342,7 +342,6 @@ body {
|
|
|
342
342
|
margin-top: 6px;
|
|
343
343
|
display: flex;
|
|
344
344
|
align-items: center;
|
|
345
|
-
justify-content: space-between;
|
|
346
345
|
gap: 8px;
|
|
347
346
|
}
|
|
348
347
|
.sidebar-bottom-links {
|
|
@@ -928,6 +927,13 @@ body {
|
|
|
928
927
|
to { opacity: 1; transform: scale(1.25); }
|
|
929
928
|
}
|
|
930
929
|
|
|
930
|
+
.streaming-cursor-row {
|
|
931
|
+
margin-top: 6px;
|
|
932
|
+
line-height: 1;
|
|
933
|
+
}
|
|
934
|
+
.streaming-cursor-row .typing-cursor::after {
|
|
935
|
+
margin-left: 0;
|
|
936
|
+
}
|
|
931
937
|
.typing-cursor::after {
|
|
932
938
|
content: "▋"; display: inline-block; margin-left: 2px;
|
|
933
939
|
animation: blink 1s steps(2) infinite; color: var(--accent);
|