From 5b7f1578026a8e60c877fed12d4b42d3b2f69403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:56:51 +0800 Subject: [PATCH] Add files via upload --- web/static/i18n/en-US.json | 1 + web/static/i18n/zh-CN.json | 1 + web/static/js/chat.js | 121 ++++++++++++++++--------------------- web/static/js/projects.js | 71 +++++++++++++++------- 4 files changed, 104 insertions(+), 90 deletions(-) diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 0d6910de..9d742e5f 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2633,6 +2633,7 @@ "conversationName": "Conversation name", "project": "Project", "noProject": "No project", + "unknownProject": "Unknown project", "filterByProject": "Filter by project", "lastTime": "Last activity", "action": "Action", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index f5c68e64..08a52d71 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2621,6 +2621,7 @@ "conversationName": "对话名称", "project": "项目", "noProject": "无项目", + "unknownProject": "未知项目", "filterByProject": "按项目筛选", "lastTime": "最近一次对话时间", "action": "操作", diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 779b0bc8..e648f2f9 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -6166,9 +6166,6 @@ const CONVERSATION_PROJECT_FILTER_NONE = '__none__'; const CONVERSATION_PROJECT_FILTER_SELECT_ID = 'conversation-project-filter'; const CONVERSATION_PROJECT_FILTER_CARET = ''; const BATCH_PROJECT_FILTER_SELECT_ID = 'batch-project-filter'; -const PROJECT_FILTER_REMOTE_SEARCH_LIMIT = 50; -const PROJECT_FILTER_REMOTE_INITIAL_LIMIT = 20; -const PROJECT_FILTER_REMOTE_DEBOUNCE_MS = 300; const projectFilterCustomSelectRegistry = {}; let projectFilterCustomSelectDocBound = false; @@ -6185,11 +6182,11 @@ function closeProjectFilterCustomSelect(selectId) { if (!reg || !reg.wrapper) return; reg.wrapper.classList.remove('open'); if (reg.trigger) reg.trigger.setAttribute('aria-expanded', 'false'); - if (reg.remoteSearchTimer) { - clearTimeout(reg.remoteSearchTimer); - reg.remoteSearchTimer = null; + if (reg.filterSearchTimer) { + clearTimeout(reg.filterSearchTimer); + reg.filterSearchTimer = null; } - reg.remoteSearchSeq = (reg.remoteSearchSeq || 0) + 1; + reg.filterSearchSeq = (reg.filterSearchSeq || 0) + 1; if (reg.searchInput) reg.searchInput.value = ''; } @@ -6219,10 +6216,10 @@ function ensureProjectFilterSearchUi(reg) { optionsList.className = 'conversation-project-filter-options'; dropdown.appendChild(optionsList); reg.optionsList = optionsList; - reg.remoteSearchSeq = 0; - reg.remoteSearchTimer = null; + reg.filterSearchSeq = 0; + reg.filterSearchTimer = null; - searchInput.addEventListener('input', () => scheduleProjectFilterRemoteSearch(reg.select.id)); + searchInput.addEventListener('input', () => loadProjectFilterLocalOptions(reg.select.id)); searchInput.addEventListener('click', (e) => e.stopPropagation()); searchInput.addEventListener('keydown', (e) => { e.stopPropagation(); @@ -6283,60 +6280,39 @@ function ensureNativeProjectFilterOption(select, projectId, label) { select.appendChild(opt); } -function scheduleProjectFilterRemoteSearch(selectId) { - const reg = projectFilterCustomSelectRegistry[selectId]; - if (!reg) return; - if (reg.remoteSearchTimer) clearTimeout(reg.remoteSearchTimer); - reg.remoteSearchTimer = setTimeout(() => { - reg.remoteSearchTimer = null; - loadProjectFilterRemoteOptions(selectId); - }, PROJECT_FILTER_REMOTE_DEBOUNCE_MS); -} - -async function queryProjectFilterRemote(query, limit) { - if (typeof window.searchActiveProjects === 'function') { - return window.searchActiveProjects(query, { limit }); - } - const params = new URLSearchParams({ status: 'active', limit: String(limit) }); - const q = String(query || '').trim(); - if (q) params.set('search', q); - const res = await apiFetch(`/api/projects?${params}`); - if (!res.ok) throw new Error('search failed'); - const data = await res.json(); - const items = data.projects || data.items || (Array.isArray(data) ? data : []); - if (typeof window.rememberProjectsInNameMap === 'function') { - window.rememberProjectsInNameMap(items); - } - return { - items, - total: typeof data.total === 'number' ? data.total : items.length, - }; -} - -async function loadProjectFilterRemoteOptions(selectId) { +async function loadProjectFilterLocalOptions(selectId) { const reg = projectFilterCustomSelectRegistry[selectId]; if (!reg || !reg.optionsList) return; const query = (reg.searchInput?.value || '').trim(); - const seq = ++reg.remoteSearchSeq; + const seq = ++reg.filterSearchSeq; - renderProjectFilterPinnedOptions(reg); - const loadingEl = appendProjectFilterStatusMessage( - reg.optionsList, - 'conversation-project-filter-status', - projectFilterT('chat.filterProjectSearchLoading', '搜索中…') - ); + const needsFetch = typeof window.isProjectsCacheReady === 'function' && !window.isProjectsCacheReady(); + let loadingEl = null; + if (needsFetch) { + renderProjectFilterPinnedOptions(reg); + loadingEl = appendProjectFilterStatusMessage( + reg.optionsList, + 'conversation-project-filter-status', + projectFilterT('common.loading', '加载中…') + ); + } try { - const parsed = await queryProjectFilterRemote( - query, - query ? PROJECT_FILTER_REMOTE_SEARCH_LIMIT : PROJECT_FILTER_REMOTE_INITIAL_LIMIT - ); - if (seq !== reg.remoteSearchSeq) return; + const ensureLoaded = typeof window.ensureProjectsLoaded === 'function' + ? window.ensureProjectsLoaded + : null; + const filterLocal = typeof window.filterActiveProjectsLocal === 'function' + ? window.filterActiveProjectsLocal + : null; + if (!ensureLoaded || !filterLocal) throw new Error('projects cache unavailable'); + + const all = await ensureLoaded(); + if (seq !== reg.filterSearchSeq) return; renderProjectFilterPinnedOptions(reg); const selected = reg.select.value; const pinnedValues = new Set(['', CONVERSATION_PROJECT_FILTER_NONE]); - const projects = (parsed.items || []).filter((p) => p && p.id && p.status !== 'archived'); + const projects = filterLocal(all, query); projects.forEach((p) => { if (pinnedValues.has(p.id)) return; reg.optionsList.appendChild( @@ -6350,21 +6326,9 @@ async function loadProjectFilterRemoteOptions(selectId) { 'conversation-project-filter-empty', projectFilterT('chat.filterProjectSearchEmpty', '没有匹配的项目') ); - } else if (!query && parsed.total > projects.length) { - appendProjectFilterStatusMessage( - reg.optionsList, - 'conversation-project-filter-hint', - projectFilterT('chat.filterProjectSearchMore', '更多项目请输入关键字搜索') - ); - } else if (!query && projects.length === 0) { - appendProjectFilterStatusMessage( - reg.optionsList, - 'conversation-project-filter-hint', - projectFilterT('chat.filterProjectSearchHint', '输入关键字搜索项目') - ); } } catch (e) { - if (seq !== reg.remoteSearchSeq) return; + if (seq !== reg.filterSearchSeq) return; renderProjectFilterPinnedOptions(reg); appendProjectFilterStatusMessage( reg.optionsList, @@ -6438,7 +6402,7 @@ function initProjectFilterCustomSelect(selectId) { const reg = projectFilterCustomSelectRegistry[selectId]; if (reg?.searchInput) { reg.searchInput.value = ''; - loadProjectFilterRemoteOptions(selectId); + loadProjectFilterLocalOptions(selectId); requestAnimationFrame(() => reg.searchInput.focus()); } } @@ -8425,7 +8389,25 @@ function getConversationProjectLabel(conv) { if (!pid) { return typeof window.t === 'function' ? window.t('batchManageModal.noProject') : '无项目'; } - return (window.projectNameById && window.projectNameById[pid]) || pid; + const name = window.projectNameById && window.projectNameById[pid]; + if (name) return name; + return typeof window.t === 'function' ? window.t('batchManageModal.unknownProject') : '未知项目'; +} + +async function prefetchProjectNamesForConversations(conversations) { + const missing = new Set(); + for (const conv of conversations || []) { + const pid = getConversationProjectId(conv); + if (pid && !(window.projectNameById && window.projectNameById[pid])) { + missing.add(pid); + } + } + if (!missing.size) return; + const fetchSummary = typeof window.fetchProjectSummary === 'function' + ? window.fetchProjectSummary + : null; + if (!fetchSummary) return; + await Promise.all([...missing].map((id) => fetchSummary(id).catch(() => null))); } async function refreshBatchProjectFilter() { @@ -8479,6 +8461,7 @@ async function showBatchManageModal() { try { initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID); allConversationsForBatch = await fetchAllConversations(''); + await prefetchProjectNamesForConversations(allConversationsForBatch); await refreshBatchProjectFilter(); const sidebarFilter = getConversationProjectFilter(); const batchSel = document.getElementById('batch-project-filter'); diff --git a/web/static/js/projects.js b/web/static/js/projects.js index 66c1cb50..013336ad 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -179,8 +179,34 @@ function rememberProjectsInNameMap(list) { }); } -const PROJECT_PICKER_SEARCH_LIMIT = 50; -const PROJECT_PICKER_INITIAL_LIMIT = 20; +/** 与后端 projectListSearchPattern 对齐:name / description / id 子串匹配(忽略大小写) */ +function matchProjectSearchQuery(project, query) { + const q = String(query || '').trim().toLowerCase(); + if (!q) return true; + const name = String(project.name || '').toLowerCase(); + const desc = String(project.description || '').toLowerCase(); + const id = String(project.id || '').toLowerCase(); + return name.includes(q) || desc.includes(q) || id.includes(q); +} + +function sortProjectsForPicker(projects) { + return [...projects].sort((a, b) => { + const ap = a.pinned ? 1 : 0; + const bp = b.pinned ? 1 : 0; + if (bp !== ap) return bp - ap; + const au = a.updated_at || a.updatedAt || ''; + const bu = b.updated_at || b.updatedAt || ''; + return String(bu).localeCompare(String(au)); + }); +} + +/** 从已加载列表中筛选活跃项目(对话选择器 / 项目筛选下拉) */ +function filterActiveProjectsLocal(projects, query) { + const list = (projects || []).filter((p) => p && p.id && p.status !== 'archived'); + const q = String(query || '').trim(); + const filtered = q ? list.filter((p) => matchProjectSearchQuery(p, q)) : list; + return sortProjectsForPicker(filtered); +} async function searchActiveProjects(query, opts = {}) { const params = new URLSearchParams(); @@ -341,11 +367,16 @@ async function ensureProjectsLoaded(force) { return _projectsFetchPromise; } +function isProjectsCacheReady() { + return _projectsListReady; +} + function prefetchProjectsForChat() { const id = (resolveChatProjectSelection() || '').trim(); if (id && !projectNameById[id]) { fetchProjectSummary(id).catch(() => {}); } + ensureProjectsLoaded().catch(() => {}); } /** 新对话时默认不绑定项目;用户需主动选择后才写入共享黑板 */ @@ -2108,7 +2139,7 @@ async function normalizeStaleChatProjectSelection() { } } -const PROJECT_PICKER_DEBOUNCE_MS = 300; +const PROJECT_PICKER_DEBOUNCE_MS = 100; const projectPickerPanelState = { chat: { seq: 0, timer: null }, webshell: { seq: 0, timer: null }, @@ -2174,23 +2205,25 @@ async function renderProjectPickerPanel(panelKey, config) { ); }; - list.innerHTML = ''; - renderPinned(); - const loadingEl = appendChatProjectPanelMessage( - list, - 'chat-project-panel-loading', - pickerMessage(t, 'common.loading', '加载中…') - ); + const needsFetch = !isProjectsCacheReady(); + let loadingEl = null; + if (needsFetch) { + list.innerHTML = ''; + renderPinned(); + loadingEl = appendChatProjectPanelMessage( + list, + 'chat-project-panel-loading', + pickerMessage(t, 'common.loading', '加载中…') + ); + } try { - const parsed = await searchActiveProjects(query, { - limit: query ? PROJECT_PICKER_SEARCH_LIMIT : PROJECT_PICKER_INITIAL_LIMIT, - }); + const all = await ensureProjectsLoaded(); if (seq !== state.seq) return; list.innerHTML = ''; renderPinned(); - const projects = (parsed.items || []).filter((p) => p && p.id && p.status !== 'archived'); + const projects = filterActiveProjectsLocal(all, query); projects.forEach((p) => { appendChatProjectPanelItem(list, p, selectedId, config.onSelect, t); }); @@ -2201,12 +2234,6 @@ async function renderProjectPickerPanel(panelKey, config) { 'chat-project-panel-empty', pickerMessage(t, 'chat.filterProjectSearchEmpty', '没有匹配的项目') ); - } else if (!query && parsed.total > projects.length) { - appendChatProjectPanelMessage( - list, - 'chat-project-panel-hint', - pickerMessage(t, 'chat.filterProjectSearchMore', '更多项目请输入关键字搜索') - ); } } catch (e) { if (seq !== state.seq) return; @@ -2218,7 +2245,7 @@ async function renderProjectPickerPanel(panelKey, config) { pickerMessage(t, 'chat.filterProjectSearchFailed', '加载项目失败,请重试') ); } finally { - if (loadingEl.parentNode) loadingEl.remove(); + if (loadingEl && loadingEl.parentNode) loadingEl.remove(); } } @@ -2496,6 +2523,8 @@ window.toggleProjectFactGraphConnectMode = toggleProjectFactGraphConnectMode; window.rebuildProjectNameMap = rebuildProjectNameMap; window.rememberProjectsInNameMap = rememberProjectsInNameMap; window.searchActiveProjects = searchActiveProjects; +window.filterActiveProjectsLocal = filterActiveProjectsLocal; window.fetchProjectSummary = fetchProjectSummary; window.projectNameById = projectNameById; window.ensureProjectsLoaded = ensureProjectsLoaded; +window.isProjectsCacheReady = isProjectsCacheReady;