@liguoshuai/pi-web-chat 1.7.3 → 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.
@@ -1,6 +1,6 @@
1
1
  # 架构设计文档
2
2
 
3
- > pi-web-chat 的技术架构、数据流、关键设计决策与扩展点说明(对应 v1.7.3 版本)。
3
+ > pi-web-chat 的技术架构、数据流、关键设计决策与扩展点说明(对应 v1.7.4 版本)。
4
4
 
5
5
  ---
6
6
 
package/docs/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.7.4] - 2026-07-26
11
+
12
+ ### Added
13
+ - **模型菜单实时搜索与过滤 (Model Search & Instant Filter)**:
14
+ - 顶栏模型下拉菜单增加置顶固定的搜索过滤输入框,支持根据 Provider 提供商名称、模型名称、模型 ID 进行多维度实时模糊过滤。
15
+ - 打开模型下拉框时自动聚焦并全选搜索框,支持一键清空(`×`)、按 `Escape` 快捷关闭、按 `Enter` 快捷选中。
16
+ - 优化模型列表项布局,同时展示友好显示名与底层 Model ID,解决模型列表过长时翻找困难的问题。
17
+ - 移动端与桌面端自适应滚动与粘性搜索栏布局。
18
+
19
+ ---
20
+
10
21
  ## [1.7.3] - 2026-07-26
11
22
 
12
23
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguoshuai/pi-web-chat",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
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",
package/public/app.js CHANGED
@@ -1439,29 +1439,112 @@ function renderModelPill() {
1439
1439
  pill.title = `当前模型: ${provider} / ${name} (${m.id})`;
1440
1440
  }
1441
1441
 
1442
+ let modelSearchQuery = "";
1443
+
1442
1444
  function renderModelMenu() {
1443
1445
  const menu = $("#modelMenu");
1444
- menu.innerHTML = "";
1445
- // group by provider
1446
+ if (!menu) return;
1447
+
1448
+ let searchInput = $("#modelSearchInput", menu);
1449
+ let listContainer = $(".model-menu-list", menu);
1450
+
1451
+ if (!searchInput || !listContainer) {
1452
+ menu.innerHTML = "";
1453
+ const searchWrap = el("div", { class: "model-search-wrap" });
1454
+ searchInput = el("input", {
1455
+ type: "text",
1456
+ id: "modelSearchInput",
1457
+ class: "model-search-input",
1458
+ placeholder: "搜索模型 (如 claude, deepseek, 4o)…",
1459
+ autocomplete: "off",
1460
+ spellcheck: "false",
1461
+ value: modelSearchQuery,
1462
+ });
1463
+
1464
+ searchInput.addEventListener("click", (e) => e.stopPropagation());
1465
+ searchInput.addEventListener("input", (e) => {
1466
+ modelSearchQuery = e.target.value;
1467
+ renderModelList(listContainer);
1468
+ const clearBtn = $(".btn-clear-model-search", searchWrap);
1469
+ if (clearBtn) clearBtn.style.display = modelSearchQuery ? "block" : "none";
1470
+ });
1471
+ searchInput.addEventListener("keydown", (e) => {
1472
+ if (e.key === "Escape") {
1473
+ menu.classList.remove("open");
1474
+ } else if (e.key === "Enter") {
1475
+ const visibleOpts = listContainer.querySelectorAll(".opt");
1476
+ if (visibleOpts.length > 0) {
1477
+ visibleOpts[0].click();
1478
+ }
1479
+ }
1480
+ });
1481
+
1482
+ const clearBtn = el("button", {
1483
+ class: "btn-clear-model-search",
1484
+ text: "×",
1485
+ type: "button",
1486
+ title: "清空搜索",
1487
+ style: `display: ${modelSearchQuery ? "block" : "none"}`,
1488
+ onclick: (e) => {
1489
+ e.stopPropagation();
1490
+ modelSearchQuery = "";
1491
+ searchInput.value = "";
1492
+ clearBtn.style.display = "none";
1493
+ searchInput.focus();
1494
+ renderModelList(listContainer);
1495
+ }
1496
+ });
1497
+
1498
+ searchWrap.appendChild(searchInput);
1499
+ searchWrap.appendChild(clearBtn);
1500
+ menu.appendChild(searchWrap);
1501
+
1502
+ listContainer = el("div", { class: "model-menu-list" });
1503
+ menu.appendChild(listContainer);
1504
+ }
1505
+
1506
+ renderModelList(listContainer);
1507
+ }
1508
+
1509
+ function renderModelList(listContainer) {
1510
+ if (!listContainer) return;
1511
+ listContainer.innerHTML = "";
1512
+ const q = modelSearchQuery.trim().toLowerCase();
1513
+
1514
+ const filteredModels = state.models.filter(m => {
1515
+ if (!q) return true;
1516
+ const idMatch = (m.id || "").toLowerCase().includes(q);
1517
+ const nameMatch = (m.name || "").toLowerCase().includes(q);
1518
+ const providerMatch = (m.provider || "").toLowerCase().includes(q);
1519
+ return idMatch || nameMatch || providerMatch;
1520
+ });
1521
+
1522
+ if (filteredModels.length === 0) {
1523
+ listContainer.appendChild(el("div", { class: "model-empty", text: "未找到匹配的模型" }));
1524
+ return;
1525
+ }
1526
+
1446
1527
  const groups = {};
1447
- for (const m of state.models) {
1528
+ for (const m of filteredModels) {
1448
1529
  const p = m.provider || "other";
1449
1530
  (groups[p] = groups[p] || []).push(m);
1450
1531
  }
1532
+
1451
1533
  for (const [provider, items] of Object.entries(groups).sort()) {
1452
- menu.appendChild(el("div", { class: "group-label", text: provider }));
1534
+ listContainer.appendChild(el("div", { class: "group-label", text: provider }));
1453
1535
  for (const m of items) {
1454
1536
  const active = state.currentModel && m.id === state.currentModel.id && m.provider === state.currentModel.provider;
1455
- menu.appendChild(el("div", {
1537
+ listContainer.appendChild(el("div", {
1456
1538
  class: "opt" + (active ? " active" : ""),
1457
1539
  onclick: () => {
1458
1540
  $("#modelPill").textContent = "切换中…";
1459
1541
  sendWs({ type: "set_model", provider: m.provider, modelId: m.id });
1460
- menu.classList.remove("open");
1542
+ $("#modelMenu").classList.remove("open");
1461
1543
  },
1462
1544
  }, [
1463
1545
  el("span", { class: "check", html: active ? "✓ " : "" }),
1464
- document.createTextNode(`${m.name || m.id}`),
1546
+ el("span", { class: "model-name", text: `${m.name || m.id}` }),
1547
+ (m.name && m.id && m.name !== m.id) ? el("span", { class: "model-id-sub", text: m.id }) : null,
1465
1548
  ]));
1466
1549
  }
1467
1550
  }
@@ -1649,7 +1732,18 @@ function init() {
1649
1732
  $("#modelPill").addEventListener("click", (e) => {
1650
1733
  e.stopPropagation();
1651
1734
  sendWs({ type: "get_available_models" });
1652
- $("#modelMenu").classList.toggle("open");
1735
+ const menu = $("#modelMenu");
1736
+ const willOpen = !menu.classList.contains("open");
1737
+ menu.classList.toggle("open");
1738
+ if (willOpen) {
1739
+ setTimeout(() => {
1740
+ const input = $("#modelSearchInput");
1741
+ if (input) {
1742
+ input.focus();
1743
+ input.select();
1744
+ }
1745
+ }, 50);
1746
+ }
1653
1747
  });
1654
1748
  document.addEventListener("click", (e) => {
1655
1749
  if (!e.target.closest("#modelMenu") && !e.target.closest("#modelPill")) {
package/public/style.css CHANGED
@@ -798,18 +798,97 @@ body {
798
798
  /* Model selector dropdown */
799
799
  .model-menu {
800
800
  position: absolute; top: 52px; right: 16px;
801
- background: var(--bg-input); border: 1px solid var(--border);
802
- border-radius: 12px; padding: 8px; width: 320px; max-height: 360px; overflow-y: auto;
803
- box-shadow: 0 8px 24px rgba(0,0,0,.4); z-index: 200; display: none;
801
+ background: var(--bg-sidebar); border: 1px solid var(--border);
802
+ border-radius: 14px; padding: 0; width: 340px; max-height: 440px;
803
+ box-shadow: 0 12px 32px rgba(0,0,0,.5); z-index: 200; display: none;
804
+ flex-direction: column; overflow: hidden;
804
805
  }
805
- .model-menu.open { display: block; }
806
- .model-menu .group-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; padding: 6px 8px 2px; }
807
- .model-menu .opt {
808
- padding: 8px 10px; border-radius: 8px; cursor: pointer; font-size: 13px; color: var(--text);
806
+ .model-menu.open { display: flex; }
807
+
808
+ .model-search-wrap {
809
+ padding: 8px 10px;
810
+ border-bottom: 1px solid var(--border);
811
+ background: var(--bg-sidebar);
812
+ position: relative;
813
+ display: flex;
814
+ align-items: center;
815
+ flex-shrink: 0;
816
+ }
817
+ .model-search-input {
818
+ width: 100%;
819
+ background: var(--bg-input);
820
+ border: 1px solid var(--border);
821
+ border-radius: 8px;
822
+ padding: 7px 12px;
823
+ padding-right: 28px;
824
+ color: var(--text);
825
+ font-size: 13px;
826
+ outline: none;
827
+ transition: border-color 0.15s ease;
828
+ }
829
+ .model-search-input:focus {
830
+ border-color: var(--accent);
831
+ }
832
+ .btn-clear-model-search {
833
+ position: absolute;
834
+ right: 18px;
835
+ background: transparent;
836
+ border: none;
837
+ color: var(--text-muted);
838
+ font-size: 16px;
839
+ cursor: pointer;
840
+ line-height: 1;
841
+ padding: 2px;
842
+ }
843
+ .btn-clear-model-search:hover {
844
+ color: var(--text);
845
+ }
846
+
847
+ .model-menu-list {
848
+ flex: 1;
849
+ min-height: 0;
850
+ overflow-y: auto;
851
+ padding: 6px;
852
+ max-height: 360px;
853
+ }
854
+ .model-menu-list .group-label {
855
+ font-size: 11px;
856
+ color: var(--text-dim);
857
+ text-transform: uppercase;
858
+ padding: 6px 8px 3px;
859
+ font-weight: 600;
860
+ letter-spacing: 0.5px;
861
+ }
862
+ .model-menu-list .opt {
863
+ padding: 8px 10px;
864
+ border-radius: 8px;
865
+ cursor: pointer;
866
+ font-size: 13px;
867
+ color: var(--text);
868
+ display: flex;
869
+ align-items: center;
870
+ gap: 6px;
871
+ transition: background 0.12s;
872
+ }
873
+ .model-menu-list .opt:hover { background: var(--bg-hover); }
874
+ .model-menu-list .opt.active { background: var(--bg-hover); color: var(--accent); }
875
+ .model-menu-list .opt .check { color: var(--accent); width: 14px; flex-shrink: 0; font-size: 12px; }
876
+ .model-menu-list .opt .model-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
877
+ .model-menu-list .opt .model-id-sub {
878
+ font-size: 11px;
879
+ color: var(--text-dim);
880
+ font-family: "SF Mono", Menlo, Consolas, monospace;
881
+ max-width: 120px;
882
+ overflow: hidden;
883
+ text-overflow: ellipsis;
884
+ white-space: nowrap;
885
+ }
886
+ .model-empty {
887
+ padding: 24px 12px;
888
+ text-align: center;
889
+ font-size: 13px;
890
+ color: var(--text-dim);
809
891
  }
810
- .model-menu .opt:hover { background: var(--bg-hover); }
811
- .model-menu .opt.active { background: var(--bg-hover); }
812
- .model-menu .opt .check { color: var(--accent); margin-right: 6px; }
813
892
 
814
893
 
815
894
  /* ==========================================================================
@@ -942,6 +1021,9 @@ body {
942
1021
  max-height: calc(100dvh - 70px);
943
1022
  border-radius: 12px;
944
1023
  }
1024
+ .model-menu-list {
1025
+ max-height: calc(100dvh - 130px);
1026
+ }
945
1027
 
946
1028
  .mobile-toolbar-fab {
947
1029
  display: flex;