mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-01 10:15:37 +02:00
Add files via upload
This commit is contained in:
@@ -2633,6 +2633,7 @@
|
||||
"conversationName": "Conversation name",
|
||||
"project": "Project",
|
||||
"noProject": "No project",
|
||||
"unknownProject": "Unknown project",
|
||||
"filterByProject": "Filter by project",
|
||||
"lastTime": "Last activity",
|
||||
"action": "Action",
|
||||
|
||||
@@ -2621,6 +2621,7 @@
|
||||
"conversationName": "对话名称",
|
||||
"project": "项目",
|
||||
"noProject": "无项目",
|
||||
"unknownProject": "未知项目",
|
||||
"filterByProject": "按项目筛选",
|
||||
"lastTime": "最近一次对话时间",
|
||||
"action": "操作",
|
||||
|
||||
+52
-69
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user