myagent-ai 1.7.2 → 1.7.4
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/package.json +2 -2
- package/web/api_server.py +40 -13
- package/web/ui/chat/chat.css +166 -1
- package/web/ui/chat/chat_container.html +3 -0
- package/web/ui/chat/chat_main.js +150 -11
- package/web/ui/chat/flow_engine.js +185 -57
- package/web/ui/chat/groupchat.js +8 -9
- package/web/ui/chat/middle_chat.html +3 -0
- package/web/ui/chat/right_agents.html +6 -1
- package/web/ui/index.html +43 -2
- package/agents/__pycache__/main_agent.cpython-312.pyc +0 -0
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagent-ai",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
4
4
|
"description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
|
|
5
5
|
"main": "main.py",
|
|
6
6
|
"bin": {
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
"departments/",
|
|
66
66
|
"web/"
|
|
67
67
|
]
|
|
68
|
-
}
|
|
68
|
+
}
|
package/web/api_server.py
CHANGED
|
@@ -746,29 +746,41 @@ class ApiServer:
|
|
|
746
746
|
return ""
|
|
747
747
|
|
|
748
748
|
base_instruction = (
|
|
749
|
-
"你当前处于【执行模式】(Execution Mode)。\n"
|
|
750
|
-
"
|
|
751
|
-
"
|
|
752
|
-
"
|
|
749
|
+
"你当前处于【执行模式】(Execution Mode)。\n\n"
|
|
750
|
+
"## 核心规则\n"
|
|
751
|
+
"1. **任务列表(强制)**:每次回复【必须】包含一个 ```tasklist``` 代码块,内含 JSON 数组格式的任务进度。\n"
|
|
752
|
+
" - 格式:```tasklist\\n[{\"text\": \"步骤描述\", \"status\": \"pending\"}]\\n```\n"
|
|
753
|
+
" - status 可选值:pending(待执行)、running(进行中)、done(已完成)、blocked(受阻)\n"
|
|
754
|
+
" - 首次收到任务时,拆分为多个步骤,全部标记为 pending\n"
|
|
755
|
+
" - 每次执行完一个步骤后,更新对应步骤状态为 done,下一个为 running\n"
|
|
756
|
+
"2. **单步执行(强制)**:每次回复【只能执行一个操作】(一个工具调用、一个代码块或一个技能调用)。\n"
|
|
757
|
+
" - 执行完一个操作后停下来,等待结果反馈后再决定下一步\n"
|
|
758
|
+
" - 不要一次性执行多个操作\n"
|
|
759
|
+
"3. **回复格式**:先写纯文本分析/总结 → 再写 ```tasklist``` 更新进度 → 最后写 ```action``` 执行操作(如有)\n"
|
|
753
760
|
)
|
|
754
761
|
|
|
755
762
|
# 从内存读取当前任务列表
|
|
756
763
|
tasks = self._task_list_store.get(agent_path, [])
|
|
757
764
|
if not tasks:
|
|
758
|
-
return base_instruction + "
|
|
765
|
+
return base_instruction + "\n## 当前状态\n暂无任务计划。请先分析用户需求,拆分为具体步骤,然后用 ```tasklist``` 输出计划。"
|
|
759
766
|
|
|
760
767
|
pending = [f" - ⏳ {t['text']}" for t in tasks if t.get("status") in ("pending", "running", "blocked")]
|
|
761
768
|
done = [f" - ✅ {t['text']}" for t in tasks if t.get("status") == "done"]
|
|
762
769
|
running = [f" - 🔄 {t['text']}" for t in tasks if t.get("status") == "running"]
|
|
763
770
|
|
|
764
|
-
context = base_instruction + "\n
|
|
771
|
+
context = base_instruction + "\n## 当前任务进度\n"
|
|
765
772
|
if done:
|
|
766
773
|
context += "已完成:\n" + "\n".join(done) + "\n"
|
|
767
774
|
if running:
|
|
768
775
|
context += "进行中:\n" + "\n".join(running) + "\n"
|
|
769
776
|
if pending:
|
|
770
777
|
context += "待执行:\n" + "\n".join(pending) + "\n"
|
|
771
|
-
context +=
|
|
778
|
+
context += (
|
|
779
|
+
"\n## 下一步\n"
|
|
780
|
+
"1. 用纯文本简要分析当前进展\n"
|
|
781
|
+
"2. 用 ```tasklist``` 更新任务进度(标记已完成的步骤为 done,标记当前步骤为 running)\n"
|
|
782
|
+
"3. 用 ```action``` 执行下一个待执行步骤(每次只执行一个操作)\n"
|
|
783
|
+
)
|
|
772
784
|
return context
|
|
773
785
|
|
|
774
786
|
async def handle_chat_page(self, request):
|
|
@@ -2613,7 +2625,9 @@ class ApiServer:
|
|
|
2613
2625
|
iteration = 0
|
|
2614
2626
|
# 追踪连续无 action 迭代次数,防止无限重新提示
|
|
2615
2627
|
_consecutive_no_action = 0
|
|
2616
|
-
_MAX_NO_ACTION_RETRIES =
|
|
2628
|
+
_MAX_NO_ACTION_RETRIES = 5 # 提高重试次数,给 LLM 更多机会完成剩余任务
|
|
2629
|
+
# ── 追踪所有流式推送的纯文本(用于刷新后恢复) ──
|
|
2630
|
+
_all_streamed_text_parts = [] # 每轮迭代推送的纯文本片段
|
|
2617
2631
|
|
|
2618
2632
|
while iteration < max_iter:
|
|
2619
2633
|
iteration += 1
|
|
@@ -2682,6 +2696,7 @@ class ApiServer:
|
|
|
2682
2696
|
text_before = remaining[:marker_pos]
|
|
2683
2697
|
if text_before.strip():
|
|
2684
2698
|
await _write_sse({"type": "text_delta", "content": text_before})
|
|
2699
|
+
_all_streamed_text_parts.append(text_before)
|
|
2685
2700
|
# 跳过整个开始标记(```action 或 ```tasklist),不要只跳到 ```
|
|
2686
2701
|
st["processed_pos"] += marker_pos + len(f"```{block_type}")
|
|
2687
2702
|
if block_type == "tasklist":
|
|
@@ -2718,7 +2733,9 @@ class ApiServer:
|
|
|
2718
2733
|
# 没有找到标记,流式推送(保留末尾可能的部分标记)
|
|
2719
2734
|
safe_end = len(remaining) - _MAX_HOLD
|
|
2720
2735
|
if safe_end > 0:
|
|
2721
|
-
|
|
2736
|
+
chunk = remaining[:safe_end]
|
|
2737
|
+
await _write_sse({"type": "text_delta", "content": chunk})
|
|
2738
|
+
_all_streamed_text_parts.append(chunk)
|
|
2722
2739
|
st["processed_pos"] += safe_end
|
|
2723
2740
|
remaining = full_text_so_far[st["processed_pos"]:]
|
|
2724
2741
|
else:
|
|
@@ -2790,6 +2807,7 @@ class ApiServer:
|
|
|
2790
2807
|
await _stream_text_chunked(remaining, _write_sse, chunk_size=3, delay=0.01)
|
|
2791
2808
|
else:
|
|
2792
2809
|
await _write_sse({"type": "text_delta", "content": remaining})
|
|
2810
|
+
_all_streamed_text_parts.append(remaining)
|
|
2793
2811
|
st["processed_pos"] = len(full_text)
|
|
2794
2812
|
|
|
2795
2813
|
# Call LLM with streaming — tokens are filtered through _text_delta_callback
|
|
@@ -3054,10 +3072,19 @@ class ApiServer:
|
|
|
3054
3072
|
break
|
|
3055
3073
|
|
|
3056
3074
|
# Save assistant response to memory
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3075
|
+
# ── 优先使用流式累积文本(包含所有迭代的纯文本),回退到 final_response ──
|
|
3076
|
+
saved_response = final_response
|
|
3077
|
+
if not saved_response and _all_streamed_text_parts:
|
|
3078
|
+
saved_response = "\n\n".join(p for p in _all_streamed_text_parts if p.strip())
|
|
3079
|
+
if not saved_response and content:
|
|
3080
|
+
saved_response = content # 兜底:使用最后一轮的完整输出
|
|
3081
|
+
if agent.memory and saved_response:
|
|
3082
|
+
agent.memory.add_short_term(session_id=session_id, role="assistant", content=saved_response)
|
|
3083
|
+
elif agent.memory:
|
|
3084
|
+
# 即使为空也保存一条,防止刷新后消息丢失
|
|
3085
|
+
agent.memory.add_short_term(session_id=session_id, role="assistant", content="(执行完成,无文本回复)")
|
|
3086
|
+
|
|
3087
|
+
return saved_response or final_response or content or ""
|
|
3061
3088
|
|
|
3062
3089
|
async def _execute_actions_streaming(
|
|
3063
3090
|
self, agent, action_data: dict, context, write_sse
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -588,6 +588,13 @@ input,textarea,select{font:inherit}
|
|
|
588
588
|
}
|
|
589
589
|
.rp-section-toggle:hover{background:var(--bg4);color:var(--text)}
|
|
590
590
|
.rp-section-toggle.expanded{transform:rotate(90deg)}
|
|
591
|
+
.rp-section-action-btn{
|
|
592
|
+
width:20px;height:20px;border-radius:3px;border:none;
|
|
593
|
+
background:transparent;color:var(--text3);
|
|
594
|
+
display:inline-grid;place-items:center;flex-shrink:0;
|
|
595
|
+
cursor:pointer;transition:var(--transition);
|
|
596
|
+
}
|
|
597
|
+
.rp-section-action-btn:hover{background:var(--bg4);color:var(--accent)}
|
|
591
598
|
|
|
592
599
|
/* Master Agent Card (全权Agent) */
|
|
593
600
|
.rp-master-card{
|
|
@@ -1204,8 +1211,9 @@ input,textarea,select{font:inherit}
|
|
|
1204
1211
|
opacity:0;position:absolute;right:8px;top:50%;transform:translateY(-50%);
|
|
1205
1212
|
width:24px;height:24px;border-radius:4px;display:grid;place-items:center;
|
|
1206
1213
|
background:var(--bg2);transition:var(--transition);font-size:12px;color:var(--text3);
|
|
1214
|
+
pointer-events:none;
|
|
1207
1215
|
}
|
|
1208
|
-
.group-item:hover .group-delete{opacity:1}
|
|
1216
|
+
.group-item:hover .group-delete{opacity:1;pointer-events:auto}
|
|
1209
1217
|
.group-delete:hover{background:var(--danger);color:#fff}
|
|
1210
1218
|
|
|
1211
1219
|
/* Group Chat Messages */
|
|
@@ -1569,6 +1577,20 @@ input,textarea,select{font:inherit}
|
|
|
1569
1577
|
.exec-event-result-btn:hover{background:var(--accent-light);color:var(--accent-dark)}
|
|
1570
1578
|
.exec-event-result-btn svg{width:12px;height:12px}
|
|
1571
1579
|
|
|
1580
|
+
/* ── Inline Exec Events (Timeline Interleaved) ── */
|
|
1581
|
+
.msg-timeline{display:flex;flex-direction:column;gap:6px}
|
|
1582
|
+
.inline-exec-event{margin:2px 0;padding:8px 12px;background:var(--bg2);border-left:3px solid var(--border);border-radius:6px;font-size:13px;animation:execEventSlide .3s ease-out}
|
|
1583
|
+
.inline-exec-header{display:flex;align-items:center;gap:6px;margin-bottom:4px}
|
|
1584
|
+
.inline-exec-icon{font-size:14px}
|
|
1585
|
+
.inline-exec-title{font-weight:500;color:var(--text);font-size:12px}
|
|
1586
|
+
.inline-exec-meta{color:var(--text3);font-size:11px;margin-left:auto}
|
|
1587
|
+
.inline-exec-code{background:var(--bg);padding:6px 8px;border-radius:4px;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:12px;color:var(--text2);margin:4px 0;max-height:100px;overflow:hidden;cursor:pointer;transition:var(--transition);white-space:pre-wrap;word-break:break-all}
|
|
1588
|
+
.inline-exec-code:hover{background:var(--bg3)}
|
|
1589
|
+
.inline-exec-code.expanded{max-height:none}
|
|
1590
|
+
.inline-exec-summary{color:var(--text2);font-size:12px;margin:4px 0}
|
|
1591
|
+
.inline-exec-result-btn{background:none;border:1px solid var(--border);color:var(--text2);font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer;margin-top:4px;transition:var(--transition)}
|
|
1592
|
+
.inline-exec-result-btn:hover{background:var(--bg2);border-color:var(--accent);color:var(--accent)}
|
|
1593
|
+
|
|
1572
1594
|
/* ── Execution Result Modal ── */
|
|
1573
1595
|
.exec-result-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;animation:fadeIn .15s ease}
|
|
1574
1596
|
.exec-result-modal{background:var(--bg);border:1px solid var(--border);border-radius:12px;width:min(680px,90vw);max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.25);animation:slideUp .2s ease}
|
|
@@ -1605,6 +1627,9 @@ input,textarea,select{font:inherit}
|
|
|
1605
1627
|
[data-theme="dark"] .exec-result-modal{background:var(--bg2);border-color:var(--border)}
|
|
1606
1628
|
[data-theme="dark"] .exec-result-modal-body pre{background:#0a0c10;color:#cdd6f4}
|
|
1607
1629
|
[data-theme="dark"] .exec-result-info-item{background:var(--bg3)}
|
|
1630
|
+
[data-theme="dark"] .inline-exec-event{background:var(--bg3);border-left-color:var(--border)}
|
|
1631
|
+
[data-theme="dark"] .inline-exec-code{background:var(--bg)}
|
|
1632
|
+
[data-theme="dark"] .inline-exec-result-btn:hover{background:var(--bg4)}
|
|
1608
1633
|
|
|
1609
1634
|
.thought-block {
|
|
1610
1635
|
background: rgba(0, 0, 0, 0.03);
|
|
@@ -1626,3 +1651,143 @@ input,textarea,select{font:inherit}
|
|
|
1626
1651
|
font-family: inherit;
|
|
1627
1652
|
white-space: pre-wrap;
|
|
1628
1653
|
}
|
|
1654
|
+
|
|
1655
|
+
/* ══════════════════════════════════════════════════════
|
|
1656
|
+
── Mobile Responsive (≤768px) ──
|
|
1657
|
+
══════════════════════════════════════════════════════ */
|
|
1658
|
+
|
|
1659
|
+
/* ── Mobile Overlay Backdrop (placed outside media query so JS can always use it) ── */
|
|
1660
|
+
.mobile-overlay{
|
|
1661
|
+
position:fixed;inset:0;
|
|
1662
|
+
background:rgba(0,0,0,.4);
|
|
1663
|
+
z-index:45;
|
|
1664
|
+
opacity:0;visibility:hidden;
|
|
1665
|
+
transition:opacity .3s ease,visibility .3s ease;
|
|
1666
|
+
pointer-events:none;
|
|
1667
|
+
}
|
|
1668
|
+
.mobile-overlay.active{
|
|
1669
|
+
opacity:1;visibility:visible;
|
|
1670
|
+
pointer-events:auto;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
@media(max-width:768px){
|
|
1674
|
+
|
|
1675
|
+
/* ── Sidebar: Fixed overlay sliding from left ── */
|
|
1676
|
+
.sidebar{
|
|
1677
|
+
position:fixed;
|
|
1678
|
+
left:0;top:0;
|
|
1679
|
+
width:85vw;max-width:320px;
|
|
1680
|
+
height:100vh;
|
|
1681
|
+
z-index:50;
|
|
1682
|
+
transform:translateX(-100%);
|
|
1683
|
+
transition:transform .3s cubic-bezier(.4,0,.2,1);
|
|
1684
|
+
box-shadow:none;
|
|
1685
|
+
}
|
|
1686
|
+
.sidebar.mobile-open{
|
|
1687
|
+
transform:translateX(0);
|
|
1688
|
+
box-shadow:4px 0 24px rgba(0,0,0,.15);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/* ── Agent Panel: Fixed overlay sliding from right ── */
|
|
1692
|
+
.agent-panel{
|
|
1693
|
+
position:fixed;
|
|
1694
|
+
right:0;top:0;
|
|
1695
|
+
width:85vw;max-width:340px;
|
|
1696
|
+
height:100vh;
|
|
1697
|
+
z-index:50;
|
|
1698
|
+
transform:translateX(100%);
|
|
1699
|
+
transition:transform .3s cubic-bezier(.4,0,.2,1);
|
|
1700
|
+
box-shadow:none;
|
|
1701
|
+
}
|
|
1702
|
+
.agent-panel.mobile-open{
|
|
1703
|
+
transform:translateX(0);
|
|
1704
|
+
box-shadow:-4px 0 24px rgba(0,0,0,.15);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/* ── Hide desktop toggle buttons on mobile ── */
|
|
1708
|
+
.sidebar-toggle,
|
|
1709
|
+
.agent-toggle{
|
|
1710
|
+
display:none!important;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/* ── Header ── */
|
|
1714
|
+
.main-header{
|
|
1715
|
+
height:50px;min-height:50px;
|
|
1716
|
+
padding:0 12px;
|
|
1717
|
+
}
|
|
1718
|
+
.main-title{
|
|
1719
|
+
font-size:14px;
|
|
1720
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
1721
|
+
min-width:0;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/* ── Messages ── */
|
|
1725
|
+
.messages-container{
|
|
1726
|
+
padding:12px;
|
|
1727
|
+
}
|
|
1728
|
+
.messages-inner{
|
|
1729
|
+
max-width:100%;
|
|
1730
|
+
}
|
|
1731
|
+
.message-bubble{
|
|
1732
|
+
max-width:88%;
|
|
1733
|
+
}
|
|
1734
|
+
.message-bubble pre{
|
|
1735
|
+
font-size:12px;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/* ── Input Area ── */
|
|
1739
|
+
.input-area{
|
|
1740
|
+
padding:10px 12px;
|
|
1741
|
+
}
|
|
1742
|
+
.input-wrapper{
|
|
1743
|
+
max-width:100%;
|
|
1744
|
+
}
|
|
1745
|
+
.input-box textarea{
|
|
1746
|
+
min-height:60px;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/* ── Modals ── */
|
|
1750
|
+
.modal,
|
|
1751
|
+
.agent-modal,
|
|
1752
|
+
.platform-modal{
|
|
1753
|
+
width:95%;max-height:90vh;padding:20px;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/* ── Toast ── */
|
|
1757
|
+
.toast-container{
|
|
1758
|
+
top:12px;right:12px;left:12px;
|
|
1759
|
+
}
|
|
1760
|
+
.toast{
|
|
1761
|
+
max-width:100%;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
/* ── Welcome Card Capabilities ── */
|
|
1765
|
+
.capabilities{
|
|
1766
|
+
grid-template-columns:1fr 1fr;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/* ── Execution Timer ── */
|
|
1770
|
+
.exec-timer{
|
|
1771
|
+
max-width:100%;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/* ── Empty State ── */
|
|
1775
|
+
.empty-icon{
|
|
1776
|
+
font-size:40px;
|
|
1777
|
+
}
|
|
1778
|
+
.empty-title{
|
|
1779
|
+
font-size:16px;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/* ══════════════════════════════════════════════════════
|
|
1784
|
+
── Very Small Phones (≤480px) ──
|
|
1785
|
+
══════════════════════════════════════════════════════ */
|
|
1786
|
+
@media(max-width:480px){
|
|
1787
|
+
.message-bubble{
|
|
1788
|
+
max-width:92%;
|
|
1789
|
+
}
|
|
1790
|
+
.main-header{
|
|
1791
|
+
padding:0 8px;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -28,18 +28,119 @@ initTheme();
|
|
|
28
28
|
document.getElementById('themeToggle')?.addEventListener('click', toggleTheme);
|
|
29
29
|
|
|
30
30
|
// ── Sidebar Collapse ──
|
|
31
|
+
function isMobile() { return window.innerWidth <= 768; }
|
|
32
|
+
|
|
31
33
|
function toggleSidebar() {
|
|
32
34
|
const sidebar = document.getElementById('sidebar');
|
|
33
35
|
const toggleBtn = document.getElementById('sidebarToggle');
|
|
34
36
|
if (!sidebar) return;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (isMobile()) {
|
|
38
|
+
// Mobile: toggle as overlay
|
|
39
|
+
const overlay = document.getElementById('chatMobileOverlay');
|
|
40
|
+
const isOpen = sidebar.classList.contains('mobile-open');
|
|
41
|
+
if (isOpen) {
|
|
42
|
+
closeMobileSidebar();
|
|
43
|
+
} else {
|
|
44
|
+
sidebar.classList.add('mobile-open');
|
|
45
|
+
if (overlay) overlay.classList.add('active');
|
|
46
|
+
// Close agent panel if open
|
|
47
|
+
closeMobileAgentPanel();
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// Desktop: collapse/expand
|
|
51
|
+
sidebar.classList.toggle('collapsed');
|
|
52
|
+
const isCollapsed = sidebar.classList.contains('collapsed');
|
|
53
|
+
if (toggleBtn) toggleBtn.textContent = isCollapsed ? '▶' : '◀';
|
|
54
|
+
localStorage.setItem('myagent-sidebar-collapsed', isCollapsed);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function closeMobileSidebar() {
|
|
59
|
+
const sidebar = document.getElementById('sidebar');
|
|
60
|
+
const overlay = document.getElementById('chatMobileOverlay');
|
|
61
|
+
if (sidebar) sidebar.classList.remove('mobile-open');
|
|
62
|
+
if (overlay) overlay.classList.remove('active');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toggleMobileAgentPanel() {
|
|
66
|
+
const panel = document.getElementById('agentPanel');
|
|
67
|
+
const overlay = document.getElementById('chatMobileOverlay');
|
|
68
|
+
if (!panel) return;
|
|
69
|
+
const isOpen = panel.classList.contains('mobile-open');
|
|
70
|
+
if (isOpen) {
|
|
71
|
+
closeMobileAgentPanel();
|
|
72
|
+
} else {
|
|
73
|
+
panel.classList.add('mobile-open');
|
|
74
|
+
if (overlay) overlay.classList.add('active');
|
|
75
|
+
// Close sidebar if open
|
|
76
|
+
closeMobileSidebar();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function closeMobileAgentPanel() {
|
|
81
|
+
const panel = document.getElementById('agentPanel');
|
|
82
|
+
const overlay = document.getElementById('chatMobileOverlay');
|
|
83
|
+
if (panel) panel.classList.remove('mobile-open');
|
|
84
|
+
if (overlay) overlay.classList.remove('active');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Mobile overlay click closes both panels
|
|
88
|
+
document.addEventListener('click', function(e) {
|
|
89
|
+
if (e.target && e.target.id === 'chatMobileOverlay') {
|
|
90
|
+
closeMobileSidebar();
|
|
91
|
+
closeMobileAgentPanel();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Show/hide mobile agents button based on screen size
|
|
96
|
+
function checkChatMobile() {
|
|
97
|
+
const btn = document.getElementById('mobileAgentsBtn');
|
|
98
|
+
if (btn) btn.style.display = isMobile() ? 'grid' : 'none';
|
|
99
|
+
}
|
|
100
|
+
window.addEventListener('resize', checkChatMobile);
|
|
101
|
+
// Run after DOM ready
|
|
102
|
+
if (document.readyState === 'loading') {
|
|
103
|
+
document.addEventListener('DOMContentLoaded', checkChatMobile);
|
|
104
|
+
} else {
|
|
105
|
+
checkChatMobile();
|
|
39
106
|
}
|
|
40
|
-
|
|
107
|
+
|
|
108
|
+
// Override toggleAgentPanel for mobile (deferred since function is defined later)
|
|
109
|
+
var _origToggleAgentPanel = null;
|
|
110
|
+
(function() {
|
|
111
|
+
var origDef = toggleAgentPanel;
|
|
112
|
+
if (typeof origDef === 'function') {
|
|
113
|
+
_origToggleAgentPanel = origDef;
|
|
114
|
+
toggleAgentPanel = function() {
|
|
115
|
+
if (isMobile()) {
|
|
116
|
+
toggleMobileAgentPanel();
|
|
117
|
+
} else {
|
|
118
|
+
_origToggleAgentPanel();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
})();
|
|
123
|
+
// Also override after definition via setTimeout (fallback)
|
|
124
|
+
setTimeout(function() {
|
|
125
|
+
if (!_origToggleAgentPanel && typeof toggleAgentPanel === 'function') {
|
|
126
|
+
// Check if it's already the mobile version
|
|
127
|
+
var testFn = toggleAgentPanel.toString();
|
|
128
|
+
if (testFn.indexOf('toggleMobileAgentPanel') === -1) {
|
|
129
|
+
_origToggleAgentPanel = toggleAgentPanel;
|
|
130
|
+
toggleAgentPanel = function() {
|
|
131
|
+
if (isMobile()) {
|
|
132
|
+
toggleMobileAgentPanel();
|
|
133
|
+
} else {
|
|
134
|
+
_origToggleAgentPanel();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}, 0);
|
|
140
|
+
|
|
141
|
+
// Restore sidebar state (desktop only)
|
|
41
142
|
(function() {
|
|
42
|
-
if (localStorage.getItem('myagent-sidebar-collapsed') === 'true') {
|
|
143
|
+
if (localStorage.getItem('myagent-sidebar-collapsed') === 'true' && !isMobile()) {
|
|
43
144
|
const sidebar = document.getElementById('sidebar');
|
|
44
145
|
const toggleBtn = document.getElementById('sidebarToggle');
|
|
45
146
|
if (sidebar) sidebar.classList.add('collapsed');
|
|
@@ -966,6 +1067,7 @@ async function selectAgent(agentPath) {
|
|
|
966
1067
|
// 如果 loadSessions 已经 auto-selected 了 session,UI 已由 selectSession 设置好,不再覆盖
|
|
967
1068
|
|
|
968
1069
|
document.getElementById('userInput').focus();
|
|
1070
|
+
if (isMobile()) closeMobileAgentPanel();
|
|
969
1071
|
// Reload task plan if in exec mode
|
|
970
1072
|
if (state.chatMode === 'exec') loadTaskPlan();
|
|
971
1073
|
// Reset escalation and update exec mode UI
|
|
@@ -1434,9 +1536,13 @@ function newChat() {
|
|
|
1434
1536
|
async function selectSession(id) {
|
|
1435
1537
|
if (id === '__new__') {
|
|
1436
1538
|
newChat();
|
|
1539
|
+
if (isMobile()) closeMobileSidebar();
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (state.activeSessionId === id && state.messages.length > 0) {
|
|
1543
|
+
if (isMobile()) closeMobileSidebar();
|
|
1437
1544
|
return;
|
|
1438
1545
|
}
|
|
1439
|
-
if (state.activeSessionId === id && state.messages.length > 0) return;
|
|
1440
1546
|
|
|
1441
1547
|
// 重置生成状态,防止残留的 isGenerating=true 导致输入框锁定
|
|
1442
1548
|
state.isGenerating = false;
|
|
@@ -1470,6 +1576,7 @@ async function selectSession(id) {
|
|
|
1470
1576
|
}
|
|
1471
1577
|
renderMessages();
|
|
1472
1578
|
document.getElementById('userInput').focus();
|
|
1579
|
+
if (isMobile()) closeMobileSidebar();
|
|
1473
1580
|
}
|
|
1474
1581
|
|
|
1475
1582
|
async function deleteSession(id) {
|
|
@@ -1680,9 +1787,12 @@ function renderMessages() {
|
|
|
1680
1787
|
</div>` : '';
|
|
1681
1788
|
const ttsIndicator = ttsManager && ttsManager.isPlaying && ttsManager.currentMsgIndex === i ?
|
|
1682
1789
|
' <span class="tts-playing-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg></span>' : '';
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const
|
|
1790
|
+
|
|
1791
|
+
// ── Determine rendering mode and streaming indicator ──
|
|
1792
|
+
const hasParts = Array.isArray(msg.parts) && msg.parts.length > 0;
|
|
1793
|
+
const hasStreamingText = msg._streamingText && msg._streamingText.trim();
|
|
1794
|
+
const anyContent = msg.content || msg._streamingText || hasParts;
|
|
1795
|
+
const streamingIndicator = msg.streaming && !anyContent && !msg.thought ? `
|
|
1686
1796
|
<div class="streaming-indicator">
|
|
1687
1797
|
<div class="spinner"></div>
|
|
1688
1798
|
<div class="streaming-dots">
|
|
@@ -1690,13 +1800,42 @@ function renderMessages() {
|
|
|
1690
1800
|
</div>
|
|
1691
1801
|
<span style="font-weight:500">Agent 正在思考...</span>
|
|
1692
1802
|
</div>` : '';
|
|
1803
|
+
|
|
1804
|
+
// ── Timeline rendering for interleaved text + exec events ──
|
|
1805
|
+
let timelineHtml = '';
|
|
1806
|
+
if (hasParts || hasStreamingText) {
|
|
1807
|
+
let partsHtml = '';
|
|
1808
|
+
for (const part of (msg.parts || [])) {
|
|
1809
|
+
if (part.type === 'text' && part.content.trim()) {
|
|
1810
|
+
partsHtml += '<div class="message-bubble">' + renderMarkdown(part.content) + '</div>';
|
|
1811
|
+
} else if (part.type === 'exec') {
|
|
1812
|
+
partsHtml += renderInlineExecEvent(part.data, i);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
if (hasStreamingText) {
|
|
1816
|
+
partsHtml += '<div class="message-bubble">' + renderMarkdown(msg._streamingText) + '</div>';
|
|
1817
|
+
}
|
|
1818
|
+
if (partsHtml) {
|
|
1819
|
+
timelineHtml = '<div class="msg-timeline">' + partsHtml + '</div>';
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Backward compat: single bubble for messages without parts
|
|
1824
|
+
const singleBubbleHtml = (!hasParts && !hasStreamingText)
|
|
1825
|
+
? ((content || streamingIndicator) ? `<div class="message-bubble">${content}${ttsIndicator}</div>` : '')
|
|
1826
|
+
: '';
|
|
1827
|
+
|
|
1828
|
+
// Exec events panel: only for backward compat (messages without parts loaded from DB)
|
|
1829
|
+
const execEventsHtml = (!isUser && !hasParts && msg.exec_events && msg.exec_events.length > 0)
|
|
1830
|
+
? renderExecEvents(msg.exec_events, i) : '';
|
|
1693
1831
|
html += `
|
|
1694
1832
|
<div class="message-row ${msg.role}">
|
|
1695
1833
|
<div class="message-avatar">${avatar}</div>
|
|
1696
1834
|
<div style="flex:1;min-width:0">
|
|
1697
1835
|
${reasoningHtml}
|
|
1698
1836
|
${thoughtHtml}
|
|
1699
|
-
${
|
|
1837
|
+
${timelineHtml}
|
|
1838
|
+
${singleBubbleHtml}
|
|
1700
1839
|
${streamingIndicator}
|
|
1701
1840
|
${execEventsHtml}
|
|
1702
1841
|
${msg.time ? `<div class="message-time">${formatTime(msg.time)}</div>` : ''}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<string>:27: SyntaxWarning: invalid escape sequence '\s'
|
|
2
1
|
// ══════════════════════════════════════════════════════
|
|
3
2
|
// ── Flow Engine: 文本处理流引擎 ──
|
|
4
3
|
// ── 负责消息发送、SSE 流式处理、大文本检测与分段、
|
|
@@ -351,22 +350,87 @@ function updateStreamingMessage(msgIdx) {
|
|
|
351
350
|
}
|
|
352
351
|
}
|
|
353
352
|
|
|
354
|
-
// Update content bubble
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
353
|
+
// Update content - timeline (interleaved text + exec events) or single bubble (backward compat)
|
|
354
|
+
const hasParts = Array.isArray(msg.parts);
|
|
355
|
+
if (hasParts) {
|
|
356
|
+
// ── Timeline rendering for interleaved text + exec events ──
|
|
357
|
+
let timeline = contentArea.querySelector('.msg-timeline');
|
|
358
|
+
if (!timeline) {
|
|
359
|
+
// Remove old single bubble if exists
|
|
360
|
+
const oldBubble = contentArea.querySelector(':scope > .message-bubble');
|
|
361
|
+
if (oldBubble) oldBubble.remove();
|
|
362
|
+
// Create timeline container
|
|
363
|
+
timeline = document.createElement('div');
|
|
364
|
+
timeline.className = 'msg-timeline';
|
|
365
|
+
// Insert after thought blocks or at beginning
|
|
366
|
+
const allThoughts = contentArea.querySelectorAll(':scope > .thought-block');
|
|
367
|
+
if (allThoughts.length > 0) {
|
|
368
|
+
allThoughts[allThoughts.length - 1].insertAdjacentElement('afterend', timeline);
|
|
369
|
+
} else {
|
|
370
|
+
contentArea.appendChild(timeline);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Cache completed parts rendering (only re-render when parts count changes)
|
|
375
|
+
const partsCount = msg.parts.length;
|
|
376
|
+
if (!msg._renderedPartsHtml || msg._lastPartsCount !== partsCount) {
|
|
377
|
+
let html = '';
|
|
378
|
+
for (const part of msg.parts) {
|
|
379
|
+
if (part.type === 'text' && part.content.trim()) {
|
|
380
|
+
html += '<div class="message-bubble">' + renderMarkdown(part.content) + '</div>';
|
|
381
|
+
} else if (part.type === 'exec') {
|
|
382
|
+
html += renderInlineExecEvent(part.data, msgIdx);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
msg._renderedPartsHtml = html;
|
|
386
|
+
msg._lastPartsCount = partsCount;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Build streaming bubble for current in-progress text
|
|
390
|
+
const streamingText = msg._streamingText || '';
|
|
391
|
+
const streamingBubbleHtml = streamingText.trim()
|
|
392
|
+
? '<div class="message-bubble">' + renderMarkdown(streamingText) + '</div>'
|
|
393
|
+
: '';
|
|
394
|
+
|
|
395
|
+
timeline.innerHTML = msg._renderedPartsHtml + streamingBubbleHtml;
|
|
396
|
+
|
|
397
|
+
// Remove exec events panel if present (events are now inline in timeline)
|
|
398
|
+
const execPanel = contentArea.querySelector('.exec-events-panel');
|
|
399
|
+
if (execPanel) execPanel.remove();
|
|
400
|
+
} else {
|
|
401
|
+
// ── Backward compat: single content bubble + exec events panel ──
|
|
402
|
+
let bubble = contentArea.querySelector('.message-bubble');
|
|
403
|
+
const content = renderMarkdown(msg.content);
|
|
404
|
+
if (content && !bubble) {
|
|
405
|
+
bubble = document.createElement('div');
|
|
406
|
+
bubble.className = 'message-bubble';
|
|
407
|
+
contentArea.appendChild(bubble);
|
|
408
|
+
}
|
|
409
|
+
if (bubble && content) {
|
|
410
|
+
bubble.innerHTML = content;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Exec events panel (only for backward compat messages without parts)
|
|
414
|
+
if (msg.exec_events && msg.exec_events.length > 0) {
|
|
415
|
+
let execPanel = contentArea.querySelector('.exec-events-panel');
|
|
416
|
+
const newExecHtml = renderExecEvents(msg.exec_events, msgIdx);
|
|
417
|
+
if (execPanel) {
|
|
418
|
+
execPanel.outerHTML = newExecHtml;
|
|
419
|
+
} else {
|
|
420
|
+
const timeEl = contentArea.querySelector('.message-time');
|
|
421
|
+
if (timeEl) {
|
|
422
|
+
timeEl.insertAdjacentHTML('beforebegin', newExecHtml);
|
|
423
|
+
} else {
|
|
424
|
+
contentArea.insertAdjacentHTML('beforeend', newExecHtml);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
365
428
|
}
|
|
366
429
|
|
|
367
430
|
// Update streaming indicator
|
|
368
431
|
let indicator = contentArea.querySelector('.streaming-indicator');
|
|
369
|
-
const
|
|
432
|
+
const anyContent = msg.content || msg._streamingText || (msg.parts && msg.parts.length > 0);
|
|
433
|
+
const streamingIndicator = msg.streaming && !anyContent && !msg.thought ? `
|
|
370
434
|
<div class="streaming-indicator">
|
|
371
435
|
<div class="streaming-dots">
|
|
372
436
|
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
|
@@ -381,23 +445,6 @@ function updateStreamingMessage(msgIdx) {
|
|
|
381
445
|
indicator.remove();
|
|
382
446
|
}
|
|
383
447
|
|
|
384
|
-
// Update exec events panel
|
|
385
|
-
if (msg.exec_events && msg.exec_events.length > 0) {
|
|
386
|
-
let execPanel = contentArea.querySelector('.exec-events-panel');
|
|
387
|
-
const newExecHtml = renderExecEvents(msg.exec_events, msgIdx);
|
|
388
|
-
if (execPanel) {
|
|
389
|
-
execPanel.outerHTML = newExecHtml;
|
|
390
|
-
} else {
|
|
391
|
-
// Insert before time element or at end
|
|
392
|
-
const timeEl = contentArea.querySelector('.message-time');
|
|
393
|
-
if (timeEl) {
|
|
394
|
-
timeEl.insertAdjacentHTML('beforebegin', newExecHtml);
|
|
395
|
-
} else {
|
|
396
|
-
contentArea.insertAdjacentHTML('beforeend', newExecHtml);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
448
|
// Auto-scroll
|
|
402
449
|
scrollToBottom();
|
|
403
450
|
}
|
|
@@ -508,6 +555,52 @@ function toggleExecEventsPanel(header) {
|
|
|
508
555
|
body.classList.toggle('expanded');
|
|
509
556
|
}
|
|
510
557
|
|
|
558
|
+
// ══════════════════════════════════════════════════════
|
|
559
|
+
// ── Inline Exec Event (Timeline Card) ──
|
|
560
|
+
// ══════════════════════════════════════════════════════
|
|
561
|
+
|
|
562
|
+
function renderInlineExecEvent(data, msgIdx) {
|
|
563
|
+
const iconEmoji = getEventIconEmoji(data);
|
|
564
|
+
const title = data.title || (data.tool_name || data.skill_name || '执行事件');
|
|
565
|
+
|
|
566
|
+
// Build meta text
|
|
567
|
+
let metaParts = [];
|
|
568
|
+
if (data.execution_time !== undefined) metaParts.push('耗时 ' + data.execution_time + 's');
|
|
569
|
+
if (data.language) metaParts.push(escapeHtml(data.language));
|
|
570
|
+
if (data.tool_name || data.skill_name) metaParts.push(escapeHtml(data.tool_name || data.skill_name));
|
|
571
|
+
if (data.timed_out) metaParts.push('超时');
|
|
572
|
+
if (data.exit_code !== undefined) metaParts.push('exit: ' + data.exit_code);
|
|
573
|
+
const metaText = metaParts.join(' · ');
|
|
574
|
+
|
|
575
|
+
// Build body content
|
|
576
|
+
let bodyHtml = '';
|
|
577
|
+
// Code preview for code_exec/code_result
|
|
578
|
+
if (data.code_preview && (data.type === 'code_exec' || data.type === 'code_result')) {
|
|
579
|
+
bodyHtml += '<div class="inline-exec-code" onclick="showExecResultModal(' + msgIdx + ', ' + data.id + ')" title="点击查看完整结果">' + escapeHtml(data.code_preview) + '</div>';
|
|
580
|
+
}
|
|
581
|
+
// Summary for tool_result/skill_result
|
|
582
|
+
if (data.summary && (data.type === 'tool_result' || data.type === 'skill_result')) {
|
|
583
|
+
bodyHtml += '<div class="inline-exec-summary">' + escapeHtml(data.summary) + '</div>';
|
|
584
|
+
}
|
|
585
|
+
// Result button for code_result
|
|
586
|
+
if (data.type === 'code_result' && (data.stdout || data.stderr || data.error)) {
|
|
587
|
+
bodyHtml += '<button class="inline-exec-result-btn" onclick="showExecResultModal(' + msgIdx + ', ' + data.id + ')">查看详情</button>';
|
|
588
|
+
}
|
|
589
|
+
// Result button for tool_result/skill_result
|
|
590
|
+
if ((data.type === 'tool_result' || data.type === 'skill_result') && data.result) {
|
|
591
|
+
bodyHtml += '<button class="inline-exec-result-btn" onclick="showToolResultModal(' + msgIdx + ', ' + data.id + ')">查看详情</button>';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return '<div class="inline-exec-event">' +
|
|
595
|
+
'<div class="inline-exec-header">' +
|
|
596
|
+
'<span class="inline-exec-icon">' + iconEmoji + '</span>' +
|
|
597
|
+
'<span class="inline-exec-title">' + escapeHtml(title) + '</span>' +
|
|
598
|
+
(metaText ? '<span class="inline-exec-meta">' + metaText + '</span>' : '') +
|
|
599
|
+
'</div>' +
|
|
600
|
+
bodyHtml +
|
|
601
|
+
'</div>';
|
|
602
|
+
}
|
|
603
|
+
|
|
511
604
|
// ══════════════════════════════════════════════════════
|
|
512
605
|
// ── Execution Result Modal (执行结果弹窗) ──
|
|
513
606
|
// ══════════════════════════════════════════════════════
|
|
@@ -703,14 +796,22 @@ async function sendMessage() {
|
|
|
703
796
|
const reader = resp.body.getReader();
|
|
704
797
|
const decoder = new TextDecoder();
|
|
705
798
|
let buffer = '';
|
|
706
|
-
let
|
|
799
|
+
let msgParts = []; // Timeline: [{type:'text', content:'...'}, {type:'exec', data:{...}}]
|
|
800
|
+
let currentText = ''; // Accumulator for current streaming text segment
|
|
801
|
+
let allExecEvents = []; // All exec events (for summary panel at bottom)
|
|
707
802
|
let msgIdx = state.messages.length;
|
|
708
803
|
let sessionIdReceived = sessionId;
|
|
709
|
-
let execEventsReceived = [];
|
|
710
804
|
let fullThought = '';
|
|
711
|
-
|
|
805
|
+
|
|
806
|
+
function flushCurrentText() {
|
|
807
|
+
if (currentText.trim()) {
|
|
808
|
+
msgParts.push({type: 'text', content: currentText});
|
|
809
|
+
}
|
|
810
|
+
currentText = '';
|
|
811
|
+
}
|
|
812
|
+
|
|
712
813
|
// Add placeholder for streaming response
|
|
713
|
-
state.messages.push({ role: 'assistant', content: '', thought: '', time: new Date().toISOString(), streaming: true });
|
|
814
|
+
state.messages.push({ role: 'assistant', content: '', thought: '', parts: [], time: new Date().toISOString(), streaming: true });
|
|
714
815
|
renderMessages();
|
|
715
816
|
|
|
716
817
|
while (true) {
|
|
@@ -731,13 +832,22 @@ async function sendMessage() {
|
|
|
731
832
|
// Sync the actual session ID (backend may prefix with agent_path)
|
|
732
833
|
state.activeSessionId = evt.session_id;
|
|
733
834
|
} else if (evt.type === 'text') {
|
|
734
|
-
|
|
835
|
+
// Full text event (non-streaming replacement)
|
|
836
|
+
flushCurrentText();
|
|
837
|
+
msgParts.push({type: 'text', content: evt.content});
|
|
838
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
839
|
+
state.messages[msgIdx]._streamingText = '';
|
|
735
840
|
state.messages[msgIdx].content = evt.content;
|
|
736
841
|
renderMessages();
|
|
737
842
|
} else if (evt.type === 'text_delta') {
|
|
738
843
|
// Incremental streaming token
|
|
739
|
-
|
|
740
|
-
|
|
844
|
+
currentText += evt.content;
|
|
845
|
+
// Build backward-compat content from all parts + streaming text
|
|
846
|
+
const allText = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n')
|
|
847
|
+
+ (currentText.trim() ? '\n\n' + currentText : '');
|
|
848
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
849
|
+
state.messages[msgIdx]._streamingText = currentText;
|
|
850
|
+
state.messages[msgIdx].content = allText;
|
|
741
851
|
throttledStreamUpdate(msgIdx);
|
|
742
852
|
// ── 分段流式 TTS:推送增量文本 ──
|
|
743
853
|
if (ttsManager.enabled && !ttsManager._streamActive) {
|
|
@@ -759,28 +869,39 @@ async function sendMessage() {
|
|
|
759
869
|
state.messages[msgIdx].thought = fullThought;
|
|
760
870
|
throttledStreamUpdate(msgIdx);
|
|
761
871
|
} else if (evt.type === 'queue_start') {
|
|
762
|
-
//
|
|
872
|
+
// Finalize previous message
|
|
873
|
+
flushCurrentText();
|
|
763
874
|
if (state.messages[msgIdx]) {
|
|
764
875
|
state.messages[msgIdx].streaming = false;
|
|
765
|
-
|
|
876
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
877
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
|
|
878
|
+
state.messages[msgIdx]._streamingText = '';
|
|
879
|
+
if (allExecEvents.length > 0) state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
766
880
|
}
|
|
881
|
+
// Start new message
|
|
767
882
|
state.messages.push({ role: 'user', content: evt.message, time: new Date().toISOString() });
|
|
768
883
|
msgIdx = state.messages.length;
|
|
769
|
-
|
|
884
|
+
msgParts = [];
|
|
885
|
+
currentText = '';
|
|
886
|
+
allExecEvents = [];
|
|
770
887
|
fullThought = '';
|
|
771
|
-
|
|
772
|
-
state.messages.push({ role: 'assistant', content: '', thought: '', time: new Date().toISOString(), streaming: true });
|
|
888
|
+
state.messages.push({ role: 'assistant', content: '', thought: '', parts: [], time: new Date().toISOString(), streaming: true });
|
|
773
889
|
renderMessages();
|
|
774
890
|
} else if (evt.type === 'clear_text') {
|
|
775
891
|
// Clear intermediate text from previous agent loop iterations
|
|
776
|
-
|
|
777
|
-
state.messages[msgIdx].
|
|
892
|
+
flushCurrentText();
|
|
893
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
894
|
+
state.messages[msgIdx]._streamingText = '';
|
|
895
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '';
|
|
778
896
|
throttledStreamUpdate(msgIdx);
|
|
779
897
|
} else if (evt.type === 'exec_event') {
|
|
780
898
|
// Real-time execution event (tool call, code exec, skill result, etc.)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
899
|
+
flushCurrentText();
|
|
900
|
+
msgParts.push({type: 'exec', data: evt.data});
|
|
901
|
+
allExecEvents.push(evt.data);
|
|
902
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
903
|
+
state.messages[msgIdx]._streamingText = '';
|
|
904
|
+
state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
784
905
|
throttledStreamUpdate(msgIdx);
|
|
785
906
|
} else if (evt.type === 'task_list_update') {
|
|
786
907
|
// 任务列表 JSON 直推更新(exec 模式)
|
|
@@ -803,11 +924,15 @@ async function sendMessage() {
|
|
|
803
924
|
}
|
|
804
925
|
}
|
|
805
926
|
} else if (evt.type === 'done') {
|
|
927
|
+
flushCurrentText();
|
|
806
928
|
// done 事件提供最终事件列表(可能有去重/合并)
|
|
807
929
|
if (evt.exec_events && evt.exec_events.length > 0) {
|
|
808
|
-
|
|
809
|
-
state.messages[msgIdx].exec_events = [...execEventsReceived];
|
|
930
|
+
allExecEvents = evt.exec_events;
|
|
810
931
|
}
|
|
932
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
933
|
+
state.messages[msgIdx]._streamingText = '';
|
|
934
|
+
state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
935
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
|
|
811
936
|
} else if (evt.type === 'reasoning_delta') {
|
|
812
937
|
// 模型推理过程增量文本(OpenAI o1/o3/DeepSeek-R1 等推理模型)
|
|
813
938
|
if (!state.messages[msgIdx].reasoning) state.messages[msgIdx].reasoning = '';
|
|
@@ -818,22 +943,25 @@ async function sendMessage() {
|
|
|
818
943
|
state.messages[msgIdx].reasoning = evt.content;
|
|
819
944
|
throttledStreamUpdate(msgIdx);
|
|
820
945
|
} else if (evt.type === 'error') {
|
|
821
|
-
|
|
822
|
-
|
|
946
|
+
flushCurrentText();
|
|
947
|
+
currentText = '❌ ' + evt.error;
|
|
948
|
+
msgParts.push({type: 'text', content: currentText});
|
|
949
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
950
|
+
state.messages[msgIdx]._streamingText = '';
|
|
951
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n');
|
|
823
952
|
}
|
|
824
953
|
} catch (e) { /* skip malformed */ }
|
|
825
954
|
}
|
|
826
955
|
}
|
|
827
956
|
|
|
828
957
|
// Finalize message
|
|
958
|
+
flushCurrentText();
|
|
829
959
|
if (state.messages[msgIdx]) {
|
|
830
960
|
state.messages[msgIdx].streaming = false;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
state.messages[msgIdx].content = '(无回复)';
|
|
836
|
-
}
|
|
961
|
+
state.messages[msgIdx].parts = [...msgParts];
|
|
962
|
+
state.messages[msgIdx]._streamingText = '';
|
|
963
|
+
state.messages[msgIdx].exec_events = allExecEvents;
|
|
964
|
+
state.messages[msgIdx].content = msgParts.filter(p => p.type === 'text').map(p => p.content).join('\n\n') || '(无回复)';
|
|
837
965
|
}
|
|
838
966
|
|
|
839
967
|
// Task list 已通过 SSE task_list_update 事件实时推送,无需再轮询
|
package/web/ui/chat/groupchat.js
CHANGED
|
@@ -37,7 +37,7 @@ async function fetchGroups() {
|
|
|
37
37
|
async function createGroupApi(name, description, emoji, color, members) {
|
|
38
38
|
return await api('/api/groups', {
|
|
39
39
|
method: 'POST',
|
|
40
|
-
body: JSON.stringify({ name, description, emoji, color, members }),
|
|
40
|
+
body: JSON.stringify({ name, description, avatar_emoji: emoji, avatar_color: color, members }),
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -109,13 +109,12 @@ function renderGroups() {
|
|
|
109
109
|
var memberCount = (g.members || []).length;
|
|
110
110
|
var isActive = g.id === currentGroupId;
|
|
111
111
|
html += '<div class="group-item ' + (isActive ? 'active' : '') + '" onclick="selectGroup(\'' + escapeHtml(g.id) + '\')" title="' + escapeHtml(g.name) + '">';
|
|
112
|
-
html += '<div class="group-icon">' + escapeHtml(g.emoji || '👥') + '</div>';
|
|
112
|
+
html += '<div class="group-icon"' + (g.avatar_color ? ' style="background:' + escapeHtml(g.avatar_color) + ';color:#fff"' : '') + '>' + escapeHtml(g.avatar_emoji || g.emoji || '👥') + '</div>';
|
|
113
113
|
html += '<div class="group-info">';
|
|
114
114
|
html += '<div class="group-name">' + escapeHtml(g.name) + '</div>';
|
|
115
115
|
html += '<div class="group-preview">' + escapeHtml(g.description || memberCount + ' 位成员') + '</div>';
|
|
116
116
|
html += '</div>';
|
|
117
117
|
html += '<span class="group-badge">' + memberCount + '</span>';
|
|
118
|
-
html += '<button class="group-delete" onclick="event.stopPropagation();deleteGroupConfirm(\'' + escapeHtml(g.id) + '\')" title="删除">✕</button>';
|
|
119
118
|
html += '</div>';
|
|
120
119
|
}
|
|
121
120
|
listEl.innerHTML = html;
|
|
@@ -145,7 +144,7 @@ async function selectGroup(gid) {
|
|
|
145
144
|
var groupData = await getGroup(gid);
|
|
146
145
|
|
|
147
146
|
// Update header
|
|
148
|
-
document.getElementById('headerTitle').textContent = (groupData.emoji || '👥') + ' ' + groupData.name;
|
|
147
|
+
document.getElementById('headerTitle').textContent = (groupData.avatar_emoji || groupData.emoji || '👥') + ' ' + groupData.name;
|
|
149
148
|
document.getElementById('activeAgentBadge').style.display = 'none';
|
|
150
149
|
document.getElementById('groupBackBtn').style.display = '';
|
|
151
150
|
document.getElementById('groupSettingsBtn').style.display = '';
|
|
@@ -217,7 +216,7 @@ function renderGroupMessages() {
|
|
|
217
216
|
if (groupMessages.length === 0) {
|
|
218
217
|
var group = groups.find(function(g) { return g.id === currentGroupId; });
|
|
219
218
|
html = '<div class="welcome-card">'
|
|
220
|
-
+ '<h2><span class="emoji">' + escapeHtml((group && group.emoji) || '👥') + '</span>'
|
|
219
|
+
+ '<h2><span class="emoji">' + escapeHtml((group && (group.avatar_emoji || group.emoji)) || '👥') + '</span>'
|
|
221
220
|
+ ' <span>群聊: ' + escapeHtml((group && group.name) || '') + '</span></h2>'
|
|
222
221
|
+ '<p class="subtitle">向所有成员发送消息,每个 Agent 会分别回复</p>'
|
|
223
222
|
+ '</div>';
|
|
@@ -426,8 +425,8 @@ async function showGroupSettingsModal() {
|
|
|
426
425
|
+ '</div>'
|
|
427
426
|
+ '<div class="agent-form-group"><label>头像</label>'
|
|
428
427
|
+ '<div class="agent-avatar-picker" id="groupSettingsEmojiPicker">'
|
|
429
|
-
+ emojis.map(function(e) { return '<div class="agent-avatar-option ' + ((groupData.emoji || '👥') === e ? 'selected' : '') + '" onclick="pickGroupSettingsEmoji(this,\'' + e + '\')" data-emoji="' + e + '">' + e + '</div>'; }).join('')
|
|
430
|
-
+ '</div><input type="hidden" id="groupSettingsEmoji" value="' + escapeHtml(groupData.emoji || '👥') + '"></div>'
|
|
428
|
+
+ emojis.map(function(e) { return '<div class="agent-avatar-option ' + ((groupData.avatar_emoji || groupData.emoji || '👥') === e ? 'selected' : '') + '" onclick="pickGroupSettingsEmoji(this,\'' + e + '\')" data-emoji="' + e + '">' + e + '</div>'; }).join('')
|
|
429
|
+
+ '</div><input type="hidden" id="groupSettingsEmoji" value="' + escapeHtml(groupData.avatar_emoji || groupData.emoji || '👥') + '"></div>'
|
|
431
430
|
+ '<div class="agent-form-group"><label>成员 (' + members.length + ')</label>'
|
|
432
431
|
+ '<div style="margin-bottom:8px">'
|
|
433
432
|
+ '<button class="config-action-btn" onclick="showAddMemberToGroup()" style="font-size:12px;padding:6px 12px">'
|
|
@@ -463,14 +462,14 @@ async function saveGroupSettings() {
|
|
|
463
462
|
await updateGroup(currentGroupId, {
|
|
464
463
|
name: name,
|
|
465
464
|
description: document.getElementById('groupSettingsDesc').value.trim(),
|
|
466
|
-
|
|
465
|
+
avatar_emoji: document.getElementById('groupSettingsEmoji').value
|
|
467
466
|
});
|
|
468
467
|
toast('群聊设置已保存', 'success');
|
|
469
468
|
closeGroupModal();
|
|
470
469
|
await fetchGroups();
|
|
471
470
|
var group = groups.find(function(g) { return g.id === currentGroupId; });
|
|
472
471
|
if (group) {
|
|
473
|
-
document.getElementById('headerTitle').textContent = (group.emoji || '👥') + ' ' + group.name;
|
|
472
|
+
document.getElementById('headerTitle').textContent = (group.avatar_emoji || group.emoji || '👥') + ' ' + group.name;
|
|
474
473
|
}
|
|
475
474
|
} catch (e) {
|
|
476
475
|
toast('保存失败: ' + e.message, 'error');
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
</span>
|
|
13
13
|
</div>
|
|
14
14
|
<div class="header-actions">
|
|
15
|
+
<button class="header-btn" id="mobileAgentsBtn" onclick="toggleMobileAgentPanel()" title="Agents" style="display:none">
|
|
16
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
17
|
+
</button>
|
|
15
18
|
<button class="header-btn" id="groupBackBtn" onclick="exitGroupChat()" title="返回对话" style="display:none">
|
|
16
19
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
|
17
20
|
</button>
|
|
@@ -39,7 +39,12 @@
|
|
|
39
39
|
<div id="rpGroupSection">
|
|
40
40
|
<div class="rp-section-header" onclick="toggleRpSection('group')">
|
|
41
41
|
<span class="rp-section-title">👥 群聊</span>
|
|
42
|
-
<
|
|
42
|
+
<div style="display:flex;align-items:center;gap:4px">
|
|
43
|
+
<button class="rp-section-action-btn" onclick="event.stopPropagation();showCreateGroupModal()" title="创建群聊">
|
|
44
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
45
|
+
</button>
|
|
46
|
+
<span class="rp-section-toggle expanded" id="rpGroupToggle">▶</span>
|
|
47
|
+
</div>
|
|
43
48
|
</div>
|
|
44
49
|
<div class="rp-section-body" id="rpGroupBody">
|
|
45
50
|
<div class="group-list" id="groupList">
|
package/web/ui/index.html
CHANGED
|
@@ -151,6 +151,29 @@ tr:hover{background:var(--surface2)}
|
|
|
151
151
|
[data-theme="claude"] .badge-red{background:#c9444422}
|
|
152
152
|
[data-theme="claude"] .badge-yellow{background:#c4862b22}
|
|
153
153
|
[data-theme="claude"] .badge-blue{background:#4a7fc922}
|
|
154
|
+
/* ── Mobile Responsive ── */
|
|
155
|
+
@media(max-width:768px){
|
|
156
|
+
body{flex-direction:column}
|
|
157
|
+
.sidebar{position:fixed;left:0;top:0;height:100vh;width:260px;max-width:80vw;z-index:50;transform:translateX(-100%);transition:transform .3s ease;flex-shrink:0}
|
|
158
|
+
.sidebar.mobile-open{transform:translateX(0)}
|
|
159
|
+
.sidebar.collapsed{width:260px;transform:translateX(-100%)}
|
|
160
|
+
.sidebar.collapsed.mobile-open{transform:translateX(0)}
|
|
161
|
+
.sidebar-toggle{display:none!important}
|
|
162
|
+
.mobile-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:45;display:none}
|
|
163
|
+
.mobile-overlay.active{display:block}
|
|
164
|
+
.header{padding:12px 16px}
|
|
165
|
+
.header h2{font-size:16px}
|
|
166
|
+
.content{padding:16px}
|
|
167
|
+
.grid{grid-template-columns:1fr}
|
|
168
|
+
.form-row{grid-template-columns:1fr}
|
|
169
|
+
.table-wrap{overflow-x:auto}
|
|
170
|
+
.modal{width:95%;max-height:90vh;padding:16px}
|
|
171
|
+
.modal-wide{max-width:95%}
|
|
172
|
+
.tabs{gap:0;overflow-x:auto}
|
|
173
|
+
.toast{left:16px;right:16px;bottom:16px}
|
|
174
|
+
.agent-card{flex-direction:column;align-items:flex-start}
|
|
175
|
+
.agent-card .flex.flex-col{flex-direction:row;gap:4px}
|
|
176
|
+
}
|
|
154
177
|
</style>
|
|
155
178
|
</head>
|
|
156
179
|
<body>
|
|
@@ -181,8 +204,9 @@ tr:hover{background:var(--surface2)}
|
|
|
181
204
|
· <a href="#" onclick="checkUpdate(true)" style="color:var(--primary)">检查更新</a>
|
|
182
205
|
</div>
|
|
183
206
|
</div>
|
|
207
|
+
<div class="mobile-overlay" id="adminMobileOverlay" onclick="closeMobileSidebar()"></div>
|
|
184
208
|
<div class="main">
|
|
185
|
-
<div class="header"><div style="display:flex;align-items:center;gap:12px"><h2 id="pageTitle">📊 仪表盘</h2><button class="header-btn" id="themeToggle" title="切换主题"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div><div style="display:flex;align-items:center;gap:8px"><span class="status-dot"></span>运行中</div></div>
|
|
209
|
+
<div class="header"><div style="display:flex;align-items:center;gap:12px"><button class="header-btn" id="mobileMenuBtn" onclick="toggleMobileSidebar()" style="display:none" title="菜单"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button><h2 id="pageTitle">📊 仪表盘</h2><button class="header-btn" id="themeToggle" title="切换主题"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div><div style="display:flex;align-items:center;gap:8px"><span class="status-dot"></span>运行中</div></div>
|
|
186
210
|
<div class="content" id="content"></div>
|
|
187
211
|
</div>
|
|
188
212
|
<div id="modalContainer"></div>
|
|
@@ -250,12 +274,28 @@ function initTheme(){const s=localStorage.getItem('myagent-theme')||'claude';doc
|
|
|
250
274
|
function toggleTheme(){const c=document.documentElement.getAttribute('data-theme')||'claude';const n=c==='claude'?'dark':'claude';document.documentElement.setAttribute('data-theme',n);localStorage.setItem('myagent-theme',n);updateThemeIcon(n)}
|
|
251
275
|
function updateThemeIcon(t){const b=document.getElementById('themeToggle');if(!b)return;if(t==='dark'){b.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';b.title='切换到 Claude 风格'}else{b.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';b.title='切换到夜间模式'}}
|
|
252
276
|
// ── Sidebar Collapse ──
|
|
253
|
-
function toggleSidebar(){const s=document.getElementById('adminSidebar');const t=document.getElementById('sidebarToggle');if(!s)return;s.classList.toggle('collapsed');const isCollapsed=s.classList.contains('collapsed');t.textContent=isCollapsed?'▶':'◀';localStorage.setItem('myagent-admin-sidebar-collapsed',isCollapsed)}
|
|
277
|
+
function toggleSidebar(){const s=document.getElementById('adminSidebar');const t=document.getElementById('sidebarToggle');if(!s)return;s.classList.toggle('collapsed');const isCollapsed=s.classList.contains('collapsed');t.textContent=isCollapsed?'▶':'◀';localStorage.setItem('myagent-admin-sidebar-collapsed',isCollapsed);closeMobileSidebar()}
|
|
254
278
|
// Initialize
|
|
255
279
|
initTheme();
|
|
256
280
|
document.getElementById('themeToggle')?.addEventListener('click',toggleTheme);
|
|
257
281
|
if(localStorage.getItem('myagent-admin-sidebar-collapsed')==='true'){document.getElementById('adminSidebar')?.classList.add('collapsed');const t=document.getElementById('sidebarToggle');if(t)t.textContent='▶'}
|
|
258
282
|
|
|
283
|
+
// ── Mobile Sidebar ──
|
|
284
|
+
function toggleMobileSidebar(){
|
|
285
|
+
const s=document.getElementById('adminSidebar');
|
|
286
|
+
const o=document.getElementById('adminMobileOverlay');
|
|
287
|
+
s.classList.toggle('mobile-open');
|
|
288
|
+
o.classList.toggle('active');
|
|
289
|
+
}
|
|
290
|
+
function closeMobileSidebar(){
|
|
291
|
+
document.getElementById('adminSidebar').classList.remove('mobile-open');
|
|
292
|
+
document.getElementById('adminMobileOverlay').classList.remove('active');
|
|
293
|
+
}
|
|
294
|
+
// Show hamburger on mobile
|
|
295
|
+
function checkMobile(){const btn=document.getElementById('mobileMenuBtn');if(btn)btn.style.display=window.innerWidth<=768?'grid':'none'}
|
|
296
|
+
window.addEventListener('resize',checkMobile);
|
|
297
|
+
checkMobile();
|
|
298
|
+
|
|
259
299
|
loadVersion();
|
|
260
300
|
setTimeout(()=>checkUpdate(false),30000);
|
|
261
301
|
function showConfirm(title,msg,onOk){
|
|
@@ -266,6 +306,7 @@ function showConfirm(title,msg,onOk){
|
|
|
266
306
|
}
|
|
267
307
|
|
|
268
308
|
function showPage(page){
|
|
309
|
+
closeMobileSidebar();
|
|
269
310
|
currentPage=page;
|
|
270
311
|
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===page));
|
|
271
312
|
$('pageTitle').textContent=pages[page]||page;
|
|
Binary file
|
|
Binary file
|