Add files via upload

This commit is contained in:
公明
2026-07-01 15:56:51 +08:00
committed by GitHub
parent 09890db635
commit 5b7f157802
4 changed files with 104 additions and 90 deletions
+1
View File
@@ -2633,6 +2633,7 @@
"conversationName": "Conversation name",
"project": "Project",
"noProject": "No project",
"unknownProject": "Unknown project",
"filterByProject": "Filter by project",
"lastTime": "Last activity",
"action": "Action",
+1
View File
@@ -2621,6 +2621,7 @@
"conversationName": "对话名称",
"project": "项目",
"noProject": "无项目",
"unknownProject": "未知项目",
"filterByProject": "按项目筛选",
"lastTime": "最近一次对话时间",
"action": "操作",
+52 -69
View File
@@ -6166,9 +6166,6 @@ const CONVERSATION_PROJECT_FILTER_NONE = '__none__';
const CONVERSATION_PROJECT_FILTER_SELECT_ID = 'conversation-project-filter';
const CONVERSATION_PROJECT_FILTER_CARET = '<svg class="conversation-project-filter-caret" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
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');
+50 -21
View File
@@ -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;