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:
@@ -2257,18 +2257,80 @@ header {
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
max-height: 280px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
max-height: 320px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.conversation-project-filter-ui.open .conversation-project-filter-dropdown {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.conversation-project-filter-search {
|
||||
flex-shrink: 0;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.conversation-project-filter-search-input {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.conversation-project-filter-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.conversation-project-filter-search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.conversation-project-filter-options {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.conversation-project-filter-empty {
|
||||
padding: 10px 12px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.conversation-project-filter-hint,
|
||||
.conversation-project-filter-status {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.conversation-project-filter-status {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.conversation-project-filter-option[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.conversation-project-filter-option {
|
||||
@@ -26976,6 +27038,37 @@ body.app-modal-open {
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.chat-project-panel-search {
|
||||
flex-shrink: 0;
|
||||
padding: 0 0 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.chat-project-panel-search-input {
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.chat-project-panel-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
.chat-project-panel-search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.chat-project-panel-hint {
|
||||
padding: 10px 4px 4px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.chat-project-panel .role-selection-list-main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
@@ -504,6 +504,12 @@
|
||||
"filterByProject": "Filter by project",
|
||||
"filterAllProjects": "All projects",
|
||||
"filterUnboundProjects": "Unbound",
|
||||
"filterProjectSearch": "Search projects…",
|
||||
"filterProjectSearchEmpty": "No matching projects",
|
||||
"filterProjectSearchHint": "Type to search projects",
|
||||
"filterProjectSearchMore": "Type to find more projects",
|
||||
"filterProjectSearchLoading": "Searching…",
|
||||
"filterProjectSearchFailed": "Failed to load projects. Try again.",
|
||||
"projectConversationsTitle": "{{name}} · Conversations",
|
||||
"unboundConversationsTitle": "Unbound conversations",
|
||||
"noProjectConversations": "No conversations in this project",
|
||||
|
||||
@@ -492,6 +492,12 @@
|
||||
"filterByProject": "按项目筛选",
|
||||
"filterAllProjects": "全部项目",
|
||||
"filterUnboundProjects": "未绑定项目",
|
||||
"filterProjectSearch": "搜索项目…",
|
||||
"filterProjectSearchEmpty": "没有匹配的项目",
|
||||
"filterProjectSearchHint": "输入关键字搜索项目",
|
||||
"filterProjectSearchMore": "更多项目请输入关键字搜索",
|
||||
"filterProjectSearchLoading": "搜索中…",
|
||||
"filterProjectSearchFailed": "加载项目失败,请重试",
|
||||
"projectConversationsTitle": "{{name}} · 对话",
|
||||
"unboundConversationsTitle": "未绑定项目",
|
||||
"noProjectConversations": "该项目暂无对话",
|
||||
|
||||
+251
-103
@@ -6166,52 +6166,222 @@ 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;
|
||||
|
||||
function projectFilterT(key, fallback) {
|
||||
if (typeof window.t === 'function') {
|
||||
const value = window.t(key);
|
||||
if (value && value !== key) return value;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function closeProjectFilterCustomSelect(selectId) {
|
||||
const reg = projectFilterCustomSelectRegistry[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;
|
||||
}
|
||||
reg.remoteSearchSeq = (reg.remoteSearchSeq || 0) + 1;
|
||||
if (reg.searchInput) reg.searchInput.value = '';
|
||||
}
|
||||
|
||||
function closeAllProjectFilterCustomSelects() {
|
||||
Object.keys(projectFilterCustomSelectRegistry).forEach(closeProjectFilterCustomSelect);
|
||||
}
|
||||
|
||||
function ensureProjectFilterSearchUi(reg) {
|
||||
if (reg.searchInput && reg.optionsList) return;
|
||||
const { dropdown } = reg;
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
const searchWrap = document.createElement('div');
|
||||
searchWrap.className = 'conversation-project-filter-search';
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'search';
|
||||
searchInput.className = 'conversation-project-filter-search-input';
|
||||
searchInput.setAttribute('autocomplete', 'off');
|
||||
searchInput.setAttribute('data-i18n', 'chat.filterProjectSearch');
|
||||
searchInput.setAttribute('data-i18n-attr', 'placeholder');
|
||||
searchInput.placeholder = projectFilterT('chat.filterProjectSearch', '搜索项目…');
|
||||
searchWrap.appendChild(searchInput);
|
||||
dropdown.appendChild(searchWrap);
|
||||
reg.searchInput = searchInput;
|
||||
|
||||
const optionsList = document.createElement('div');
|
||||
optionsList.className = 'conversation-project-filter-options';
|
||||
dropdown.appendChild(optionsList);
|
||||
reg.optionsList = optionsList;
|
||||
reg.remoteSearchSeq = 0;
|
||||
reg.remoteSearchTimer = null;
|
||||
|
||||
searchInput.addEventListener('input', () => scheduleProjectFilterRemoteSearch(reg.select.id));
|
||||
searchInput.addEventListener('click', (e) => e.stopPropagation());
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Escape') closeProjectFilterCustomSelect(reg.select.id);
|
||||
});
|
||||
}
|
||||
|
||||
function createProjectFilterOptionButton(value, label, selectedValue) {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'conversation-project-filter-option';
|
||||
item.setAttribute('role', 'option');
|
||||
item.setAttribute('data-value', value);
|
||||
item.title = label;
|
||||
if (value === selectedValue) {
|
||||
item.classList.add('is-selected');
|
||||
item.setAttribute('aria-selected', 'true');
|
||||
} else {
|
||||
item.setAttribute('aria-selected', 'false');
|
||||
}
|
||||
const check = document.createElement('span');
|
||||
check.className = 'conversation-project-filter-check';
|
||||
check.setAttribute('aria-hidden', 'true');
|
||||
check.textContent = '✓';
|
||||
const labelEl = document.createElement('span');
|
||||
labelEl.className = 'conversation-project-filter-option-label';
|
||||
labelEl.textContent = label;
|
||||
labelEl.title = label;
|
||||
item.appendChild(check);
|
||||
item.appendChild(labelEl);
|
||||
return item;
|
||||
}
|
||||
|
||||
function appendProjectFilterStatusMessage(optionsList, className, text) {
|
||||
const el = document.createElement('div');
|
||||
el.className = className;
|
||||
el.textContent = text;
|
||||
optionsList.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderProjectFilterPinnedOptions(reg) {
|
||||
const { select, optionsList } = reg;
|
||||
optionsList.innerHTML = '';
|
||||
Array.prototype.forEach.call(select.options, (opt) => {
|
||||
if (opt.value === '' || opt.value === CONVERSATION_PROJECT_FILTER_NONE) {
|
||||
optionsList.appendChild(createProjectFilterOptionButton(opt.value, opt.textContent || '', select.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ensureNativeProjectFilterOption(select, projectId, label) {
|
||||
if (!projectId || projectId === CONVERSATION_PROJECT_FILTER_NONE) return;
|
||||
if (Array.prototype.some.call(select.options, (opt) => opt.value === projectId)) return;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = projectId;
|
||||
opt.textContent = label || projectId;
|
||||
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) {
|
||||
const reg = projectFilterCustomSelectRegistry[selectId];
|
||||
if (!reg || !reg.optionsList) return;
|
||||
const query = (reg.searchInput?.value || '').trim();
|
||||
const seq = ++reg.remoteSearchSeq;
|
||||
|
||||
renderProjectFilterPinnedOptions(reg);
|
||||
const loadingEl = appendProjectFilterStatusMessage(
|
||||
reg.optionsList,
|
||||
'conversation-project-filter-status',
|
||||
projectFilterT('chat.filterProjectSearchLoading', '搜索中…')
|
||||
);
|
||||
|
||||
try {
|
||||
const parsed = await queryProjectFilterRemote(
|
||||
query,
|
||||
query ? PROJECT_FILTER_REMOTE_SEARCH_LIMIT : PROJECT_FILTER_REMOTE_INITIAL_LIMIT
|
||||
);
|
||||
if (seq !== reg.remoteSearchSeq) 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');
|
||||
projects.forEach((p) => {
|
||||
if (pinnedValues.has(p.id)) return;
|
||||
reg.optionsList.appendChild(
|
||||
createProjectFilterOptionButton(p.id, p.name || p.id, selected)
|
||||
);
|
||||
});
|
||||
|
||||
if (query && projects.length === 0) {
|
||||
appendProjectFilterStatusMessage(
|
||||
reg.optionsList,
|
||||
'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;
|
||||
renderProjectFilterPinnedOptions(reg);
|
||||
appendProjectFilterStatusMessage(
|
||||
reg.optionsList,
|
||||
'conversation-project-filter-empty',
|
||||
projectFilterT('chat.filterProjectSearchFailed', '加载项目失败,请重试')
|
||||
);
|
||||
} finally {
|
||||
if (loadingEl && loadingEl.parentNode) loadingEl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function syncProjectFilterCustomSelect(selectId) {
|
||||
const reg = projectFilterCustomSelectRegistry[selectId];
|
||||
if (!reg) return;
|
||||
const { select, dropdown, trigger } = reg;
|
||||
ensureProjectFilterSearchUi(reg);
|
||||
const { select, trigger } = reg;
|
||||
const valueSpan = trigger.querySelector('.conversation-project-filter-value');
|
||||
dropdown.innerHTML = '';
|
||||
Array.prototype.forEach.call(select.options, (opt) => {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'conversation-project-filter-option';
|
||||
item.setAttribute('role', 'option');
|
||||
item.setAttribute('data-value', opt.value);
|
||||
const labelText = opt.textContent || '';
|
||||
item.title = labelText;
|
||||
if (opt.value === select.value) {
|
||||
item.classList.add('is-selected');
|
||||
item.setAttribute('aria-selected', 'true');
|
||||
} else {
|
||||
item.setAttribute('aria-selected', 'false');
|
||||
}
|
||||
const check = document.createElement('span');
|
||||
check.className = 'conversation-project-filter-check';
|
||||
check.setAttribute('aria-hidden', 'true');
|
||||
check.textContent = '✓';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'conversation-project-filter-option-label';
|
||||
label.textContent = labelText;
|
||||
label.title = labelText;
|
||||
item.appendChild(check);
|
||||
item.appendChild(label);
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
const selectedOpt = select.options[select.selectedIndex];
|
||||
const selectedText = selectedOpt ? (selectedOpt.textContent || '') : '';
|
||||
if (valueSpan) {
|
||||
@@ -6264,6 +6434,13 @@ function initProjectFilterCustomSelect(selectId) {
|
||||
if (!open) {
|
||||
wrapper.classList.add('open');
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
ensureProjectFilterSearchUi(projectFilterCustomSelectRegistry[selectId]);
|
||||
const reg = projectFilterCustomSelectRegistry[selectId];
|
||||
if (reg?.searchInput) {
|
||||
reg.searchInput.value = '';
|
||||
loadProjectFilterRemoteOptions(selectId);
|
||||
requestAnimationFrame(() => reg.searchInput.focus());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6273,6 +6450,8 @@ function initProjectFilterCustomSelect(selectId) {
|
||||
e.stopPropagation();
|
||||
const val = opt.getAttribute('data-value');
|
||||
if (val === null) return;
|
||||
const label = opt.querySelector('.conversation-project-filter-option-label')?.textContent || val;
|
||||
ensureNativeProjectFilterOption(select, val, label);
|
||||
if (select.value !== val) {
|
||||
select.value = val;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
@@ -6319,38 +6498,7 @@ function setConversationProjectFilter(projectId) {
|
||||
updateConversationSidebarFilterUI();
|
||||
}
|
||||
|
||||
function isValidConversationProjectFilter(projectId) {
|
||||
if (!projectId) return true;
|
||||
if (projectId === CONVERSATION_PROJECT_FILTER_NONE) return true;
|
||||
const map = window.projectNameById;
|
||||
if (!map || typeof map !== 'object') return true;
|
||||
return Object.prototype.hasOwnProperty.call(map, projectId);
|
||||
}
|
||||
|
||||
async function refreshConversationProjectFilter() {
|
||||
const sel = document.getElementById('conversation-project-filter');
|
||||
if (!sel) return;
|
||||
const saved = getConversationProjectFilter();
|
||||
let projects = [];
|
||||
if (typeof window.ensureProjectsLoaded === 'function') {
|
||||
try {
|
||||
const list = await window.ensureProjectsLoaded();
|
||||
projects = (list || []).filter((p) => p && p.id && p.status !== 'archived');
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
if (!projects.length) {
|
||||
try {
|
||||
const res = await apiFetch('/api/projects?status=active&limit=200');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const items = data.projects || data.items || (Array.isArray(data) ? data : []);
|
||||
projects = items.filter((p) => p && p.id);
|
||||
if (typeof window.rebuildProjectNameMap === 'function') {
|
||||
window.rebuildProjectNameMap(items);
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
function appendProjectFilterPinnedNativeOptions(sel) {
|
||||
const tFn = typeof window.t === 'function' ? window.t.bind(window) : null;
|
||||
const allLabel = tFn ? tFn('chat.filterAllProjects') : '全部项目';
|
||||
const unboundLabel = tFn ? tFn('chat.filterUnboundProjects') : '未绑定项目';
|
||||
@@ -6365,16 +6513,44 @@ async function refreshConversationProjectFilter() {
|
||||
unboundOpt.textContent = unboundLabel;
|
||||
unboundOpt.setAttribute('data-i18n', 'chat.filterUnboundProjects');
|
||||
sel.appendChild(unboundOpt);
|
||||
projects
|
||||
.slice()
|
||||
.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || '', undefined, { sensitivity: 'base' }))
|
||||
.forEach((p) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name || p.id;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
const normalized = isValidConversationProjectFilter(saved) ? saved : '';
|
||||
}
|
||||
|
||||
async function resolveProjectFilterSelection(projectId) {
|
||||
const saved = (projectId || '').trim();
|
||||
if (!saved || saved === CONVERSATION_PROJECT_FILTER_NONE) return saved;
|
||||
const fetchSummary = typeof window.fetchProjectSummary === 'function'
|
||||
? window.fetchProjectSummary
|
||||
: null;
|
||||
if (!fetchSummary) return saved;
|
||||
const project = await fetchSummary(saved);
|
||||
if (!project || !project.id || project.status === 'archived') return '';
|
||||
return project.id;
|
||||
}
|
||||
|
||||
async function appendSelectedProjectFilterOption(sel, projectId) {
|
||||
const id = (projectId || '').trim();
|
||||
if (!id || id === CONVERSATION_PROJECT_FILTER_NONE) return;
|
||||
if (Array.prototype.some.call(sel.options, (opt) => opt.value === id)) return;
|
||||
const fetchSummary = typeof window.fetchProjectSummary === 'function'
|
||||
? window.fetchProjectSummary
|
||||
: null;
|
||||
const project = fetchSummary ? await fetchSummary(id) : null;
|
||||
const label = (project && (project.name || project.id)) || (window.projectNameById && window.projectNameById[id]) || id;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = id;
|
||||
opt.textContent = label;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
|
||||
async function refreshConversationProjectFilter() {
|
||||
const sel = document.getElementById('conversation-project-filter');
|
||||
if (!sel) return;
|
||||
const saved = getConversationProjectFilter();
|
||||
appendProjectFilterPinnedNativeOptions(sel);
|
||||
const normalized = await resolveProjectFilterSelection(saved);
|
||||
if (normalized && normalized !== CONVERSATION_PROJECT_FILTER_NONE) {
|
||||
await appendSelectedProjectFilterOption(sel, normalized);
|
||||
}
|
||||
if (normalized !== saved) setConversationProjectFilter(normalized);
|
||||
sel.value = normalized;
|
||||
syncConversationProjectCustomSelect();
|
||||
@@ -8256,40 +8432,12 @@ async function refreshBatchProjectFilter() {
|
||||
const sel = document.getElementById('batch-project-filter');
|
||||
if (!sel) return;
|
||||
const saved = sel.value || '';
|
||||
if (typeof window.ensureProjectsLoaded === 'function') {
|
||||
try {
|
||||
await window.ensureProjectsLoaded();
|
||||
} catch (e) { /* ignore */ }
|
||||
appendProjectFilterPinnedNativeOptions(sel);
|
||||
const normalized = await resolveProjectFilterSelection(saved);
|
||||
if (normalized && normalized !== CONVERSATION_PROJECT_FILTER_NONE) {
|
||||
await appendSelectedProjectFilterOption(sel, normalized);
|
||||
}
|
||||
const tFn = typeof window.t === 'function' ? window.t.bind(window) : null;
|
||||
const allLabel = tFn ? tFn('chat.filterAllProjects') : '全部项目';
|
||||
const unboundLabel = tFn ? tFn('chat.filterUnboundProjects') : '未绑定项目';
|
||||
sel.innerHTML = '';
|
||||
const allOpt = document.createElement('option');
|
||||
allOpt.value = '';
|
||||
allOpt.textContent = allLabel;
|
||||
allOpt.setAttribute('data-i18n', 'chat.filterAllProjects');
|
||||
sel.appendChild(allOpt);
|
||||
const unboundOpt = document.createElement('option');
|
||||
unboundOpt.value = CONVERSATION_PROJECT_FILTER_NONE;
|
||||
unboundOpt.textContent = unboundLabel;
|
||||
unboundOpt.setAttribute('data-i18n', 'chat.filterUnboundProjects');
|
||||
sel.appendChild(unboundOpt);
|
||||
const source = window.projectNameById ? Object.keys(window.projectNameById) : [];
|
||||
source
|
||||
.sort((a, b) => {
|
||||
const na = (window.projectNameById[a] || a).toLowerCase();
|
||||
const nb = (window.projectNameById[b] || b).toLowerCase();
|
||||
return na.localeCompare(nb);
|
||||
})
|
||||
.forEach((id) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = id;
|
||||
opt.textContent = window.projectNameById[id] || id;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
const valid = !saved || saved === CONVERSATION_PROJECT_FILTER_NONE || (window.projectNameById && window.projectNameById[saved]);
|
||||
sel.value = valid ? saved : '';
|
||||
sel.value = normalized;
|
||||
syncProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID);
|
||||
}
|
||||
|
||||
|
||||
+232
-69
@@ -173,6 +173,39 @@ function rebuildProjectNameMap(list) {
|
||||
});
|
||||
}
|
||||
|
||||
function rememberProjectsInNameMap(list) {
|
||||
(list || []).forEach((p) => {
|
||||
if (p && p.id) projectNameById[p.id] = p.name || p.id;
|
||||
});
|
||||
}
|
||||
|
||||
const PROJECT_PICKER_SEARCH_LIMIT = 50;
|
||||
const PROJECT_PICKER_INITIAL_LIMIT = 20;
|
||||
|
||||
async function searchActiveProjects(query, opts = {}) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('status', opts.status || 'active');
|
||||
params.set('limit', String(opts.limit ?? (String(query || '').trim() ? PROJECT_PICKER_SEARCH_LIMIT : PROJECT_PICKER_INITIAL_LIMIT)));
|
||||
params.set('offset', String(opts.offset ?? 0));
|
||||
const q = String(query || '').trim();
|
||||
if (q) params.set('search', q);
|
||||
const res = await apiFetch(`/api/projects?${params}`);
|
||||
if (!res.ok) throw new Error(tp('projects.loadProjectsFailed'));
|
||||
const parsed = parseProjectsListResponse(await res.json());
|
||||
rememberProjectsInNameMap(parsed.items);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function fetchProjectSummary(projectId) {
|
||||
const id = String(projectId || '').trim();
|
||||
if (!id) return null;
|
||||
const res = await apiFetch(`/api/projects/${encodeURIComponent(id)}`);
|
||||
if (!res.ok) return null;
|
||||
const project = await res.json();
|
||||
if (project && project.id) rememberProjectsInNameMap([project]);
|
||||
return project;
|
||||
}
|
||||
|
||||
function getProjectsListPageSize() {
|
||||
try {
|
||||
const saved = parseInt(localStorage.getItem(PROJECTS_LIST_PAGE_SIZE_KEY), 10);
|
||||
@@ -309,7 +342,10 @@ async function ensureProjectsLoaded(force) {
|
||||
}
|
||||
|
||||
function prefetchProjectsForChat() {
|
||||
ensureProjectsLoaded().catch(() => {});
|
||||
const id = (resolveChatProjectSelection() || '').trim();
|
||||
if (id && !projectNameById[id]) {
|
||||
fetchProjectSummary(id).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/** 新对话时默认不绑定项目;用户需主动选择后才写入共享黑板 */
|
||||
@@ -2032,27 +2068,20 @@ function getChatProjectSelection() {
|
||||
return getActiveProjectId();
|
||||
}
|
||||
|
||||
function isActiveChatProjectId(id) {
|
||||
if (!id) return false;
|
||||
const source = projectsCacheAll.length ? projectsCacheAll : projectsCache;
|
||||
return source.some((p) => p.id === id && p.status !== 'archived');
|
||||
}
|
||||
|
||||
/** 用于 UI:无效/已删除/无可用项目时视为未绑定 */
|
||||
/** 用于 UI:返回当前选中的项目 ID(有效性由 normalizeStaleChatProjectSelection 异步校验) */
|
||||
function resolveChatProjectSelection() {
|
||||
const raw = getChatProjectSelection();
|
||||
if (!raw) return '';
|
||||
if (!_projectsListReady) return raw;
|
||||
return isActiveChatProjectId(raw) ? raw : '';
|
||||
return getChatProjectSelection() || '';
|
||||
}
|
||||
|
||||
let _normalizingStaleProject = false;
|
||||
|
||||
/** 项目列表加载后,清除 localStorage 或对话上残留的失效项目 ID */
|
||||
/** 清除 localStorage 或对话上残留的失效项目 ID */
|
||||
async function normalizeStaleChatProjectSelection() {
|
||||
if (!_projectsListReady || _normalizingStaleProject) return;
|
||||
const raw = getChatProjectSelection();
|
||||
if (!raw || isActiveChatProjectId(raw)) return;
|
||||
if (_normalizingStaleProject) return;
|
||||
const raw = (getChatProjectSelection() || '').trim();
|
||||
if (!raw) return;
|
||||
const project = await fetchProjectSummary(raw);
|
||||
if (project && project.id && project.status !== 'archived') return;
|
||||
|
||||
_normalizingStaleProject = true;
|
||||
try {
|
||||
@@ -2079,6 +2108,175 @@ async function normalizeStaleChatProjectSelection() {
|
||||
}
|
||||
}
|
||||
|
||||
const PROJECT_PICKER_DEBOUNCE_MS = 300;
|
||||
const projectPickerPanelState = {
|
||||
chat: { seq: 0, timer: null },
|
||||
webshell: { seq: 0, timer: null },
|
||||
};
|
||||
|
||||
function appendChatProjectPanelItem(list, project, selectedId, onSelect, tFn) {
|
||||
const t = tFn || tp;
|
||||
const isNone = !project.id;
|
||||
const isSelected = isNone ? !selectedId : selectedId === project.id;
|
||||
const desc = isNone
|
||||
? (project.description || '')
|
||||
: (project.description || '').trim().slice(0, 80) || t('projects.sharedFactBoard');
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : '');
|
||||
btn.setAttribute('role', 'option');
|
||||
btn.onclick = () => onSelect(project.id || '');
|
||||
btn.innerHTML = `
|
||||
<div class="role-selection-item-icon-main">${isNone ? '—' : '📁'}</div>
|
||||
<div class="role-selection-item-content-main">
|
||||
<div class="role-selection-item-name-main">${escapeHtml(project.name || t('common.untitled'))}</div>
|
||||
<div class="role-selection-item-description-main">${escapeHtml(desc)}</div>
|
||||
</div>
|
||||
${isSelected ? '<div class="role-selection-checkmark-main">✓</div>' : ''}
|
||||
`;
|
||||
list.appendChild(btn);
|
||||
}
|
||||
|
||||
function appendChatProjectPanelMessage(list, className, text) {
|
||||
const el = document.createElement('div');
|
||||
el.className = className;
|
||||
el.textContent = text;
|
||||
list.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function pickerMessage(t, key, fallback) {
|
||||
const value = t(key);
|
||||
if (!value || value === key) return fallback;
|
||||
return value;
|
||||
}
|
||||
|
||||
async function renderProjectPickerPanel(panelKey, config) {
|
||||
const state = projectPickerPanelState[panelKey];
|
||||
const list = document.getElementById(config.listId);
|
||||
if (!list || !state) return;
|
||||
const query = (document.getElementById(config.searchInputId)?.value || '').trim();
|
||||
const seq = ++state.seq;
|
||||
const selectedId = config.getSelectedId();
|
||||
const t = config.t || tp;
|
||||
|
||||
const renderPinned = () => {
|
||||
appendChatProjectPanelItem(
|
||||
list,
|
||||
{
|
||||
id: '',
|
||||
name: t('projects.noProject'),
|
||||
description: t('projects.noProjectDescription'),
|
||||
},
|
||||
selectedId,
|
||||
config.onSelect,
|
||||
t
|
||||
);
|
||||
};
|
||||
|
||||
list.innerHTML = '';
|
||||
renderPinned();
|
||||
const 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,
|
||||
});
|
||||
if (seq !== state.seq) return;
|
||||
|
||||
list.innerHTML = '';
|
||||
renderPinned();
|
||||
const projects = (parsed.items || []).filter((p) => p && p.id && p.status !== 'archived');
|
||||
projects.forEach((p) => {
|
||||
appendChatProjectPanelItem(list, p, selectedId, config.onSelect, t);
|
||||
});
|
||||
|
||||
if (query && projects.length === 0) {
|
||||
appendChatProjectPanelMessage(
|
||||
list,
|
||||
'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;
|
||||
list.innerHTML = '';
|
||||
renderPinned();
|
||||
appendChatProjectPanelMessage(
|
||||
list,
|
||||
'chat-project-panel-empty',
|
||||
pickerMessage(t, 'chat.filterProjectSearchFailed', '加载项目失败,请重试')
|
||||
);
|
||||
} finally {
|
||||
if (loadingEl.parentNode) loadingEl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function initProjectPickerPanelSearch(panelKey, searchInputId, onSearch) {
|
||||
const input = document.getElementById(searchInputId);
|
||||
if (!input || input.dataset.pickerBound === panelKey) return;
|
||||
input.dataset.pickerBound = panelKey;
|
||||
input.addEventListener('input', onSearch);
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (panelKey === 'chat' && typeof closeChatProjectPanel === 'function') {
|
||||
closeChatProjectPanel();
|
||||
} else if (panelKey === 'webshell' && typeof wsCloseProjectPanel === 'function') {
|
||||
wsCloseProjectPanel();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearProjectPickerPanelSearch(panelKey, searchInputId) {
|
||||
const state = projectPickerPanelState[panelKey];
|
||||
if (!state) return;
|
||||
state.seq += 1;
|
||||
if (state.timer) {
|
||||
clearTimeout(state.timer);
|
||||
state.timer = null;
|
||||
}
|
||||
const input = document.getElementById(searchInputId);
|
||||
if (input) input.value = '';
|
||||
}
|
||||
|
||||
function scheduleProjectPickerPanelSearch(panelKey, loadFn) {
|
||||
const state = projectPickerPanelState[panelKey];
|
||||
if (!state) return;
|
||||
if (state.timer) clearTimeout(state.timer);
|
||||
state.timer = setTimeout(() => {
|
||||
state.timer = null;
|
||||
loadFn();
|
||||
}, PROJECT_PICKER_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
async function loadChatProjectPanelList() {
|
||||
await renderProjectPickerPanel('chat', {
|
||||
listId: 'chat-project-list',
|
||||
searchInputId: 'chat-project-search',
|
||||
getSelectedId: resolveChatProjectSelection,
|
||||
onSelect: (projectId) => selectChatProject(projectId),
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureChatProjectButtonLabel() {
|
||||
const id = (resolveChatProjectSelection() || '').trim();
|
||||
if (id && !projectNameById[id]) {
|
||||
await fetchProjectSummary(id);
|
||||
}
|
||||
updateChatProjectButtonLabel();
|
||||
}
|
||||
|
||||
function updateChatProjectButtonLabel() {
|
||||
const textEl = document.getElementById('chat-project-text');
|
||||
if (!textEl) return;
|
||||
@@ -2086,56 +2284,13 @@ function updateChatProjectButtonLabel() {
|
||||
textEl.textContent = id && projectNameById[id] ? projectNameById[id] : tp('projects.noProject');
|
||||
}
|
||||
|
||||
function renderChatProjectPanelList() {
|
||||
const list = document.getElementById('chat-project-list');
|
||||
if (!list) return;
|
||||
const selected = resolveChatProjectSelection();
|
||||
const source = projectsCacheAll.length ? projectsCacheAll : projectsCache;
|
||||
const activeProjects = source.filter((p) => p.status !== 'archived');
|
||||
const items = [{ id: '', name: tp('projects.noProject'), description: tp('projects.noProjectDescription') }, ...activeProjects];
|
||||
if (!items.length) {
|
||||
list.innerHTML = `<div class="chat-project-panel-empty">${escapeHtml(tp('projects.noProjectsClickCreate'))}</div>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '';
|
||||
items.forEach((p) => {
|
||||
const isNone = !p.id;
|
||||
const isSelected = isNone ? !selected : selected === p.id;
|
||||
const desc = isNone
|
||||
? (p.description || '')
|
||||
: (p.description || '').trim().slice(0, 80) || tp('projects.sharedFactBoard');
|
||||
const projectId = p.id || '';
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : '');
|
||||
btn.setAttribute('role', 'option');
|
||||
btn.onclick = () => {
|
||||
selectChatProject(projectId);
|
||||
};
|
||||
btn.innerHTML = `
|
||||
<div class="role-selection-item-icon-main">${isNone ? '—' : '📁'}</div>
|
||||
<div class="role-selection-item-content-main">
|
||||
<div class="role-selection-item-name-main">${escapeHtml(p.name || tp('common.untitled'))}</div>
|
||||
<div class="role-selection-item-description-main">${escapeHtml(desc)}</div>
|
||||
</div>
|
||||
${isSelected ? '<div class="role-selection-checkmark-main">✓</div>' : ''}
|
||||
`;
|
||||
list.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
async function renderChatProjectPanel() {
|
||||
const list = document.getElementById('chat-project-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = `<div class="chat-project-panel-loading">${escapeHtml(tp('common.loading'))}</div>`;
|
||||
try {
|
||||
await ensureProjectsLoaded();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
list.innerHTML = `<div class="chat-project-panel-empty">${escapeHtml(tp('projects.loadFailedRetry'))}</div>`;
|
||||
return;
|
||||
}
|
||||
renderChatProjectPanelList();
|
||||
initProjectPickerPanelSearch('chat', 'chat-project-search', () => {
|
||||
scheduleProjectPickerPanelSearch('chat', () => loadChatProjectPanelList());
|
||||
});
|
||||
clearProjectPickerPanelSearch('chat', 'chat-project-search');
|
||||
await loadChatProjectPanelList();
|
||||
requestAnimationFrame(() => document.getElementById('chat-project-search')?.focus());
|
||||
}
|
||||
|
||||
function closeChatProjectPanel() {
|
||||
@@ -2146,6 +2301,7 @@ function closeChatProjectPanel() {
|
||||
btn.classList.remove('active');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
clearProjectPickerPanelSearch('chat', 'chat-project-search');
|
||||
}
|
||||
|
||||
async function toggleChatProjectPanel() {
|
||||
@@ -2213,15 +2369,14 @@ async function applyChatProjectSelection(projectId) {
|
||||
async function refreshChatProjectSelector() {
|
||||
if (!document.getElementById('chat-project-btn')) return;
|
||||
try {
|
||||
await ensureProjectsLoaded();
|
||||
await normalizeStaleChatProjectSelection();
|
||||
await ensureChatProjectButtonLabel();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
updateChatProjectButtonLabel();
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
if (panel && panel.style.display === 'flex') {
|
||||
renderChatProjectPanelList();
|
||||
await loadChatProjectPanelList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2240,7 +2395,7 @@ function initChatProjectSelector() {
|
||||
renderProjectsPagination();
|
||||
updateChatProjectButtonLabel();
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
if (panel && panel.style.display === 'flex') renderChatProjectPanelList();
|
||||
if (panel && panel.style.display === 'flex') loadChatProjectPanelList();
|
||||
if (currentProjectId) {
|
||||
refreshProjectDetailMetaI18n();
|
||||
const source = projectsCacheAll.length ? projectsCacheAll : projectsCache;
|
||||
@@ -2298,6 +2453,11 @@ window.onChatProjectChange = onChatProjectChange;
|
||||
window.toggleChatProjectPanel = toggleChatProjectPanel;
|
||||
window.closeChatProjectPanel = closeChatProjectPanel;
|
||||
window.selectChatProject = selectChatProject;
|
||||
window.renderProjectPickerPanel = renderProjectPickerPanel;
|
||||
window.initProjectPickerPanelSearch = initProjectPickerPanelSearch;
|
||||
window.clearProjectPickerPanelSearch = clearProjectPickerPanelSearch;
|
||||
window.scheduleProjectPickerPanelSearch = scheduleProjectPickerPanelSearch;
|
||||
window.loadChatProjectPanelList = loadChatProjectPanelList;
|
||||
window.prefetchProjectsForChat = prefetchProjectsForChat;
|
||||
window.ensureDefaultActiveProjectForNewChat = ensureDefaultActiveProjectForNewChat;
|
||||
window.getActiveProjectId = getActiveProjectId;
|
||||
@@ -2334,5 +2494,8 @@ window.deleteProjectFactEdge = deleteProjectFactEdge;
|
||||
window.focusProjectFactGraphEdge = focusProjectFactGraphEdge;
|
||||
window.toggleProjectFactGraphConnectMode = toggleProjectFactGraphConnectMode;
|
||||
window.rebuildProjectNameMap = rebuildProjectNameMap;
|
||||
window.rememberProjectsInNameMap = rememberProjectsInNameMap;
|
||||
window.searchActiveProjects = searchActiveProjects;
|
||||
window.fetchProjectSummary = fetchProjectSummary;
|
||||
window.projectNameById = projectNameById;
|
||||
window.ensureProjectsLoaded = ensureProjectsLoaded;
|
||||
|
||||
+42
-41
@@ -362,6 +362,20 @@ function wsProjectT(key, fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function wsProjectPickerT(key) {
|
||||
var fallbacks = {
|
||||
'projects.noProject': '无项目',
|
||||
'projects.noProjectDescription': '不绑定项目黑板',
|
||||
'projects.sharedFactBoard': '共享事实黑板',
|
||||
'common.untitled': '未命名',
|
||||
'common.loading': '加载中…',
|
||||
'chat.filterProjectSearchEmpty': '没有匹配的项目',
|
||||
'chat.filterProjectSearchMore': '更多项目请输入关键字搜索',
|
||||
'chat.filterProjectSearchFailed': '加载项目失败,请重试',
|
||||
};
|
||||
return wsProjectT(key, fallbacks[key]);
|
||||
}
|
||||
|
||||
function getWebshellAiConvId(conn) {
|
||||
if (!conn || !conn.id) return '';
|
||||
return webshellAiConvMap[conn.id] || '';
|
||||
@@ -409,51 +423,32 @@ function wsUpdateProjectButtonLabel() {
|
||||
textEl.textContent = id && nameMap[id] ? nameMap[id] : wsProjectT('projects.noProject', '无项目');
|
||||
}
|
||||
|
||||
async function wsRenderProjectPanelList() {
|
||||
var list = document.getElementById('ws-project-list');
|
||||
if (!list || !webshellCurrentConn) return;
|
||||
var conn = webshellCurrentConn;
|
||||
var selected = wsResolveWebshellAiProjectSelection(conn);
|
||||
var projects = [];
|
||||
try {
|
||||
if (typeof window.fetchAllProjects === 'function') {
|
||||
projects = await window.fetchAllProjects(false);
|
||||
}
|
||||
} catch (e) {
|
||||
list.innerHTML = '<div class="chat-project-panel-empty">' + escapeHtml(wsProjectT('projects.loadFailedRetry', '加载失败,请重试')) + '</div>';
|
||||
return;
|
||||
}
|
||||
if (typeof window.rebuildProjectNameMap === 'function') {
|
||||
window.rebuildProjectNameMap(projects);
|
||||
}
|
||||
var activeProjects = projects.filter(function (p) { return p.status !== 'archived'; });
|
||||
var items = [{ id: '', name: wsProjectT('projects.noProject', '无项目'), description: wsProjectT('projects.noProjectDescription', '不绑定项目') }].concat(activeProjects);
|
||||
list.innerHTML = '';
|
||||
items.forEach(function (p) {
|
||||
var isNone = !p.id;
|
||||
var isSelected = isNone ? !selected : selected === p.id;
|
||||
var desc = isNone
|
||||
? (p.description || '')
|
||||
: ((p.description || '').trim().slice(0, 80) || wsProjectT('projects.sharedFactBoard', '共享事实黑板'));
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : '');
|
||||
btn.setAttribute('role', 'option');
|
||||
btn.onclick = function () { wsSelectProject(p.id || ''); };
|
||||
btn.innerHTML = '<div class="role-selection-item-icon-main">' + (isNone ? '—' : '📁') + '</div>' +
|
||||
'<div class="role-selection-item-content-main">' +
|
||||
'<div class="role-selection-item-name-main">' + escapeHtml(p.name || '未命名') + '</div>' +
|
||||
'<div class="role-selection-item-description-main">' + escapeHtml(desc) + '</div></div>' +
|
||||
(isSelected ? '<div class="role-selection-checkmark-main">✓</div>' : '');
|
||||
list.appendChild(btn);
|
||||
async function wsLoadProjectPanelList() {
|
||||
if (typeof window.renderProjectPickerPanel !== 'function') return;
|
||||
await window.renderProjectPickerPanel('webshell', {
|
||||
listId: 'ws-project-list',
|
||||
searchInputId: 'ws-project-search',
|
||||
getSelectedId: function () {
|
||||
return webshellCurrentConn ? wsResolveWebshellAiProjectSelection(webshellCurrentConn) : '';
|
||||
},
|
||||
onSelect: function (projectId) { wsSelectProject(projectId); },
|
||||
t: wsProjectPickerT,
|
||||
});
|
||||
}
|
||||
|
||||
async function wsRenderProjectPanel() {
|
||||
var list = document.getElementById('ws-project-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '<div class="chat-project-panel-loading">' + escapeHtml(wsProjectT('common.loading', '加载中...')) + '</div>';
|
||||
await wsRenderProjectPanelList();
|
||||
if (typeof window.initProjectPickerPanelSearch === 'function') {
|
||||
window.initProjectPickerPanelSearch('webshell', 'ws-project-search', function () {
|
||||
if (typeof window.scheduleProjectPickerPanelSearch === 'function') {
|
||||
window.scheduleProjectPickerPanelSearch('webshell', function () { wsLoadProjectPanelList(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (typeof window.clearProjectPickerPanelSearch === 'function') {
|
||||
window.clearProjectPickerPanelSearch('webshell', 'ws-project-search');
|
||||
}
|
||||
await wsLoadProjectPanelList();
|
||||
requestAnimationFrame(function () { document.getElementById('ws-project-search')?.focus(); });
|
||||
}
|
||||
|
||||
function wsCloseProjectPanel() {
|
||||
@@ -464,6 +459,9 @@ function wsCloseProjectPanel() {
|
||||
btn.classList.remove('active');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
if (typeof window.clearProjectPickerPanelSearch === 'function') {
|
||||
window.clearProjectPickerPanelSearch('webshell', 'ws-project-search');
|
||||
}
|
||||
}
|
||||
|
||||
async function wsToggleProjectPanel() {
|
||||
@@ -2230,6 +2228,9 @@ function selectWebshell(id, stateReady) {
|
||||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>' +
|
||||
'</div>' +
|
||||
'<div class="chat-project-panel-body">' +
|
||||
'<div class="chat-project-panel-search">' +
|
||||
'<input type="search" id="ws-project-search" class="chat-project-panel-search-input" autocomplete="off" placeholder="' + escapeHtml(wsProjectT('projects.searchProjectsPlaceholder', '搜索项目…')) + '">' +
|
||||
'</div>' +
|
||||
'<div id="ws-project-list" class="role-selection-list-main"></div>' +
|
||||
'<div class="chat-project-panel-footer">' +
|
||||
'<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromWebshellAi()">' +
|
||||
|
||||
@@ -1052,6 +1052,9 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-project-panel-body">
|
||||
<div class="chat-project-panel-search">
|
||||
<input type="search" id="chat-project-search" class="chat-project-panel-search-input" autocomplete="off" data-i18n="projects.searchProjectsPlaceholder" data-i18n-attr="placeholder" placeholder="搜索项目…">
|
||||
</div>
|
||||
<div id="chat-project-list" class="role-selection-list-main"></div>
|
||||
<div class="chat-project-panel-footer">
|
||||
<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromChat()">
|
||||
|
||||
Reference in New Issue
Block a user