mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-10 00:03:59 +02:00
Add files via upload
This commit is contained in:
@@ -1658,6 +1658,14 @@ header {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.conversations-list-empty {
|
||||
padding: 12px 10px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.conversation-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -21998,6 +22006,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 14px;
|
||||
@@ -22005,6 +22014,10 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||
overflow: hidden;
|
||||
min-height: 420px;
|
||||
}
|
||||
.projects-sidebar-head,
|
||||
.projects-sidebar-search {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.projects-sidebar-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -22049,6 +22062,122 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px 8px 10px;
|
||||
min-height: 0;
|
||||
}
|
||||
.sidebar-list-pagination {
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid #eef2f7;
|
||||
background: #fafbfc;
|
||||
padding: 6px 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.sidebar-list-pagination-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
.sidebar-list-pagination-inner--compact {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 4px 6px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.sidebar-list-pagination .pagination-info {
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sidebar-list-pagination-inner--compact .pagination-info {
|
||||
text-align: left;
|
||||
font-size: 0.6875rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.sidebar-list-pagination .pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
flex-wrap: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar-list-pagination .pagination-page {
|
||||
min-width: 2.25rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 0.6875rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.sidebar-list-pagination .pagination-page-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 3px;
|
||||
white-space: nowrap;
|
||||
font-size: 0.6875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar-list-pagination .pagination-page-size select {
|
||||
font-size: 0.6875rem;
|
||||
padding: 1px 2px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
width: 2.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.sidebar-list-pagination .btn-compact {
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
min-height: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.sidebar-list-pagination .btn-icon-pagination {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sidebar-list-pagination .btn-icon-pagination:hover:not(:disabled) {
|
||||
border-color: #0066ff;
|
||||
color: #0066ff;
|
||||
}
|
||||
.sidebar-list-pagination .btn-icon-pagination:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
.recent-conversations-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.conversation-sidebar-pagination {
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid var(--border-color, #e2e8f0);
|
||||
background: #fff;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.projects-sidebar-pagination {
|
||||
margin-top: auto;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid var(--border-color, #e2e8f0);
|
||||
background: #fff;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.conversation-sidebar.collapsed .conversation-sidebar-pagination {
|
||||
display: none;
|
||||
}
|
||||
.projects-list-item {
|
||||
position: relative;
|
||||
|
||||
@@ -236,6 +236,13 @@
|
||||
"newProjectCta": "+ New project",
|
||||
"projectList": "Project list",
|
||||
"searchProjectsPlaceholder": "Search projects…",
|
||||
"paginationShow": "Show {{start}}-{{end}} of {{total}}",
|
||||
"paginationRange": "{{start}}-{{end}}/{{total}}",
|
||||
"paginationTotal": "{{total}} total",
|
||||
"paginationPage": "{{page}}/{{total}}",
|
||||
"paginationPerPage": "Per page",
|
||||
"paginationPrev": "Previous",
|
||||
"paginationNext": "Next",
|
||||
"selectOrCreateTitle": "Select or create a project",
|
||||
"selectOrCreateHint": "Projects share a cross-chat fact board; target, environment, auth and other facts are auto-injected in bound conversations.",
|
||||
"createFirstProject": "Create first project",
|
||||
@@ -415,6 +422,13 @@
|
||||
"addGroup": "New group",
|
||||
"recentConversations": "Recent conversations",
|
||||
"batchManage": "Batch manage",
|
||||
"paginationShow": "Show {{start}}-{{end}} of {{total}}",
|
||||
"paginationRange": "{{start}}-{{end}}/{{total}}",
|
||||
"paginationTotal": "{{total}} total",
|
||||
"paginationPage": "{{page}}/{{total}}",
|
||||
"paginationPerPage": "Per page",
|
||||
"paginationPrev": "Previous",
|
||||
"paginationNext": "Next",
|
||||
"attackChain": "Attack chain",
|
||||
"viewAttackChain": "View attack chain",
|
||||
"selectRole": "Select role",
|
||||
|
||||
@@ -224,6 +224,13 @@
|
||||
"newProjectCta": "+ 新建项目",
|
||||
"projectList": "项目列表",
|
||||
"searchProjectsPlaceholder": "搜索项目…",
|
||||
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}}",
|
||||
"paginationRange": "{{start}}-{{end}}/{{total}}",
|
||||
"paginationTotal": "共 {{total}} 条",
|
||||
"paginationPage": "{{page}}/{{total}}",
|
||||
"paginationPerPage": "每页",
|
||||
"paginationPrev": "上一页",
|
||||
"paginationNext": "下一页",
|
||||
"selectOrCreateTitle": "选择或创建项目",
|
||||
"selectOrCreateHint": "项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。",
|
||||
"createFirstProject": "创建第一个项目",
|
||||
@@ -403,6 +410,13 @@
|
||||
"addGroup": "新建分组",
|
||||
"recentConversations": "最近对话",
|
||||
"batchManage": "批量管理",
|
||||
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}}",
|
||||
"paginationRange": "{{start}}-{{end}}/{{total}}",
|
||||
"paginationTotal": "共 {{total}} 条",
|
||||
"paginationPage": "{{page}}/{{total}}",
|
||||
"paginationPerPage": "每页",
|
||||
"paginationPrev": "上一页",
|
||||
"paginationNext": "下一页",
|
||||
"attackChain": "攻击链",
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"selectRole": "选择角色",
|
||||
|
||||
+199
-18
@@ -2939,6 +2939,8 @@ function createConversationListItem(conversation) {
|
||||
// 处理历史记录搜索
|
||||
let conversationSearchTimer = null;
|
||||
function handleConversationSearch(query) {
|
||||
conversationsPagination.page = 1;
|
||||
conversationsSearchQuery = query || '';
|
||||
// 防抖处理,避免频繁请求
|
||||
if (conversationSearchTimer) {
|
||||
clearTimeout(conversationSearchTimer);
|
||||
@@ -2972,6 +2974,8 @@ function clearConversationSearch() {
|
||||
clearBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
conversationsPagination.page = 1;
|
||||
conversationsSearchQuery = '';
|
||||
loadConversations('');
|
||||
}
|
||||
|
||||
@@ -5608,6 +5612,168 @@ let groupsCache = [];
|
||||
let conversationGroupMappingCache = {};
|
||||
let pendingGroupMappings = {}; // 待保留的分组映射(用于处理后端API延迟的情况)
|
||||
let conversationsListLoadSeq = 0; // 对话列表加载序号,避免并发请求导致重复渲染
|
||||
const CONVERSATIONS_PAGE_SIZE_KEY = 'cyberstrike.conversations_page_size';
|
||||
|
||||
function getConversationsPageSize() {
|
||||
try {
|
||||
const saved = parseInt(localStorage.getItem(CONVERSATIONS_PAGE_SIZE_KEY), 10);
|
||||
if ([20, 50, 100].includes(saved)) return saved;
|
||||
} catch (e) { /* ignore */ }
|
||||
return 50;
|
||||
}
|
||||
|
||||
let conversationsPagination = { page: 1, pageSize: getConversationsPageSize(), total: 0 };
|
||||
let conversationsSearchQuery = '';
|
||||
|
||||
function parseListTotalValue(raw, itemsLength) {
|
||||
if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw;
|
||||
if (raw != null && raw !== '') {
|
||||
const n = parseInt(String(raw), 10);
|
||||
if (Number.isFinite(n) && n >= 0) return n;
|
||||
}
|
||||
return itemsLength;
|
||||
}
|
||||
|
||||
function parseListOffsetValue(raw) {
|
||||
if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw;
|
||||
if (raw != null && raw !== '') {
|
||||
const n = parseInt(String(raw), 10);
|
||||
if (Number.isFinite(n) && n >= 0) return n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parseConversationsListResponse(data) {
|
||||
if (Array.isArray(data)) {
|
||||
return { items: data, total: data.length, limit: data.length, offset: 0, isLegacyArray: true };
|
||||
}
|
||||
const items = data.conversations || data.items || [];
|
||||
const arr = Array.isArray(items) ? items : [];
|
||||
return {
|
||||
items: arr,
|
||||
total: parseListTotalValue(data.total, arr.length),
|
||||
limit: parseListTotalValue(data.limit, arr.length) || arr.length,
|
||||
offset: parseListOffsetValue(data.offset),
|
||||
isLegacyArray: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveConversationsListTotal(params, parsed, pageSize, offset) {
|
||||
const serverTotal = parsed.total;
|
||||
if (!parsed.isLegacyArray && serverTotal > offset + parsed.items.length) {
|
||||
return serverTotal;
|
||||
}
|
||||
if (parsed.items.length < pageSize) {
|
||||
return Math.max(serverTotal, offset + parsed.items.length);
|
||||
}
|
||||
const probe = new URLSearchParams(params);
|
||||
probe.set('offset', String(offset + pageSize));
|
||||
probe.set('limit', '1');
|
||||
try {
|
||||
const res = await apiFetch(`/api/conversations?${probe}`);
|
||||
if (!res.ok) return Math.max(serverTotal, offset + parsed.items.length);
|
||||
const probeParsed = parseConversationsListResponse(await res.json());
|
||||
if (probeParsed.total > serverTotal) return probeParsed.total;
|
||||
if (probeParsed.items.length > 0) {
|
||||
return Math.max(serverTotal, offset + pageSize + 1);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return Math.max(serverTotal, offset + parsed.items.length);
|
||||
}
|
||||
|
||||
async function fetchAllConversations(searchQuery) {
|
||||
let all = [];
|
||||
const pageSize = 200;
|
||||
let offset = 0;
|
||||
let total = Infinity;
|
||||
const search = (searchQuery || '').trim();
|
||||
while (all.length < total) {
|
||||
const params = new URLSearchParams({ limit: String(pageSize), offset: String(offset) });
|
||||
if (search) params.set('search', search);
|
||||
const res = await apiFetch(`/api/conversations?${params}`);
|
||||
if (!res.ok) throw new Error('load conversations failed');
|
||||
const parsed = parseConversationsListResponse(await res.json());
|
||||
all = all.concat(parsed.items);
|
||||
total = parsed.total;
|
||||
if (!parsed.items.length) break;
|
||||
offset += parsed.items.length;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
function getConversationListEmptyHtml() {
|
||||
return '<div class="conversations-list-empty" data-i18n="chat.noHistoryConversations"></div>';
|
||||
}
|
||||
|
||||
function renderConversationsPagination(visibleCount) {
|
||||
const el = document.getElementById('conversations-pagination');
|
||||
if (!el) return;
|
||||
const { page, pageSize, total } = conversationsPagination;
|
||||
const count = typeof visibleCount === 'number' ? visibleCount : (conversationsPagination.visibleCount || 0);
|
||||
conversationsPagination.visibleCount = count;
|
||||
|
||||
if (count === 0 || total === 0) {
|
||||
el.innerHTML = '';
|
||||
el.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize) || 1);
|
||||
const navDisabled = totalPages <= 1;
|
||||
el.hidden = false;
|
||||
const start = (page - 1) * pageSize + 1;
|
||||
const end = Math.min(page * pageSize, total);
|
||||
const tFn = typeof window.t === 'function' ? window.t.bind(window) : null;
|
||||
const infoText = tFn
|
||||
? tFn('chat.paginationRange', { start, end, total })
|
||||
: `${start}-${end}/${total}`;
|
||||
const pageText = tFn
|
||||
? tFn('chat.paginationPage', { page, total: totalPages })
|
||||
: `${page}/${totalPages}`;
|
||||
const perPageLabel = tFn ? tFn('chat.paginationPerPage') : 'Per page';
|
||||
const prevLabel = tFn ? tFn('chat.paginationPrev') : 'Prev';
|
||||
const nextLabel = tFn ? tFn('chat.paginationNext') : 'Next';
|
||||
el.innerHTML = `
|
||||
<div class="sidebar-list-pagination-inner sidebar-list-pagination-inner--compact">
|
||||
<span class="pagination-info">${escapeHtml(infoText)}</span>
|
||||
<div class="pagination-controls">
|
||||
<button type="button" class="btn-icon-pagination" onclick="goConversationsPage(${page - 1})" ${page <= 1 || navDisabled ? 'disabled' : ''} title="${escapeHtml(prevLabel)}" aria-label="${escapeHtml(prevLabel)}">‹</button>
|
||||
<span class="pagination-page">${escapeHtml(pageText)}</span>
|
||||
<button type="button" class="btn-icon-pagination" onclick="goConversationsPage(${page + 1})" ${page >= totalPages || navDisabled ? 'disabled' : ''} title="${escapeHtml(nextLabel)}" aria-label="${escapeHtml(nextLabel)}">›</button>
|
||||
</div>
|
||||
<label class="pagination-page-size">
|
||||
${escapeHtml(perPageLabel)}
|
||||
<select id="conversations-page-size-pagination" onchange="changeConversationsPageSize()">
|
||||
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
|
||||
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
|
||||
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function goConversationsPage(page) {
|
||||
const totalPages = Math.max(1, Math.ceil((conversationsPagination.total || 0) / conversationsPagination.pageSize) || 1);
|
||||
const next = Math.min(Math.max(1, page), totalPages);
|
||||
if (next === conversationsPagination.page) return;
|
||||
conversationsPagination.page = next;
|
||||
loadConversationsWithGroups(conversationsSearchQuery);
|
||||
}
|
||||
|
||||
function changeConversationsPageSize() {
|
||||
const sel = document.getElementById('conversations-page-size-pagination');
|
||||
const newSize = sel ? parseInt(sel.value, 10) : 50;
|
||||
if (![20, 50, 100].includes(newSize)) return;
|
||||
try {
|
||||
localStorage.setItem(CONVERSATIONS_PAGE_SIZE_KEY, String(newSize));
|
||||
} catch (e) { /* ignore */ }
|
||||
conversationsPagination.pageSize = newSize;
|
||||
conversationsPagination.page = 1;
|
||||
loadConversationsWithGroups(conversationsSearchQuery);
|
||||
}
|
||||
|
||||
window.goConversationsPage = goConversationsPage;
|
||||
window.changeConversationsPageSize = changeConversationsPageSize;
|
||||
|
||||
// 加载分组列表
|
||||
async function loadGroups() {
|
||||
@@ -5704,12 +5870,17 @@ async function loadGroups() {
|
||||
async function loadConversationsWithGroups(searchQuery = '') {
|
||||
const loadSeq = ++conversationsListLoadSeq;
|
||||
try {
|
||||
// 并行加载分组列表、分组映射和对话列表(消除串行等待)
|
||||
const limit = (searchQuery && searchQuery.trim()) ? 100 : 100;
|
||||
let url = `/api/conversations?limit=${limit}`;
|
||||
conversationsSearchQuery = searchQuery || '';
|
||||
conversationsPagination.pageSize = getConversationsPageSize();
|
||||
const pageSize = conversationsPagination.pageSize;
|
||||
const offset = (conversationsPagination.page - 1) * pageSize;
|
||||
const convParams = new URLSearchParams({ limit: String(pageSize), offset: String(offset) });
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
url += '&search=' + encodeURIComponent(searchQuery.trim());
|
||||
convParams.set('search', searchQuery.trim());
|
||||
} else {
|
||||
convParams.set('exclude_grouped', 'true');
|
||||
}
|
||||
const url = `/api/conversations?${convParams}`;
|
||||
const [,, response] = await Promise.all([
|
||||
loadGroups(),
|
||||
loadConversationGroupMapping(),
|
||||
@@ -5726,23 +5897,26 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
const sidebarContent = listContainer.closest('.sidebar-content');
|
||||
const savedScrollTop = sidebarContent ? sidebarContent.scrollTop : 0;
|
||||
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;" data-i18n="chat.noHistoryConversations"></div>';
|
||||
const emptyStateHtml = getConversationListEmptyHtml();
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
// 如果响应不是200,显示空状态(友好处理,不显示错误)
|
||||
if (!response.ok) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
|
||||
renderConversationsPagination(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const conversations = await response.json();
|
||||
const data = await response.json();
|
||||
if (loadSeq !== conversationsListLoadSeq) return;
|
||||
const parsed = parseConversationsListResponse(data);
|
||||
conversationsPagination.total = await resolveConversationsListTotal(convParams, parsed, pageSize, offset);
|
||||
|
||||
// 双重保险:后端或并发情况下若出现重复ID,前端按ID去重
|
||||
const uniqueConversations = [];
|
||||
const seenConversationIds = new Set();
|
||||
(Array.isArray(conversations) ? conversations : []).forEach(conv => {
|
||||
parsed.items.forEach(conv => {
|
||||
if (!conv || !conv.id || seenConversationIds.has(conv.id)) {
|
||||
return;
|
||||
}
|
||||
@@ -5753,6 +5927,7 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
if (uniqueConversations.length === 0) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
|
||||
renderConversationsPagination(0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5863,15 +6038,29 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
fragment.appendChild(section);
|
||||
});
|
||||
|
||||
const visibleCount = pinnedConvs.length + Object.values(groups).reduce((n, arr) => n + (arr ? arr.length : 0), 0);
|
||||
conversationsPagination.visibleCount = visibleCount;
|
||||
|
||||
if (!hasSearchQuery && visibleCount === 0 && parsed.items.length > 0) {
|
||||
const totalPages = Math.max(1, Math.ceil(parsed.total / pageSize));
|
||||
if (conversationsPagination.page < totalPages) {
|
||||
conversationsPagination.page += 1;
|
||||
loadConversationsWithGroups(searchQuery);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (fragment.children.length === 0) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
|
||||
renderConversationsPagination(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadSeq !== conversationsListLoadSeq) return;
|
||||
listContainer.appendChild(fragment);
|
||||
updateActiveConversation();
|
||||
renderConversationsPagination(visibleCount);
|
||||
|
||||
// 恢复滚动位置
|
||||
if (sidebarContent) {
|
||||
@@ -5888,9 +6077,9 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
// 错误时显示空状态,而不是错误提示(更友好的用户体验)
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (listContainer) {
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;" data-i18n="chat.noHistoryConversations"></div>';
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
listContainer.innerHTML = getConversationListEmptyHtml();
|
||||
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
|
||||
renderConversationsPagination(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7004,15 +7193,7 @@ function updateBatchManageTitle(count) {
|
||||
|
||||
async function showBatchManageModal() {
|
||||
try {
|
||||
const response = await apiFetch('/api/conversations?limit=1000');
|
||||
|
||||
// 如果响应不是200,使用空数组(友好处理,不显示错误)
|
||||
if (!response.ok) {
|
||||
allConversationsForBatch = [];
|
||||
} else {
|
||||
const data = await response.json();
|
||||
allConversationsForBatch = Array.isArray(data) ? data : [];
|
||||
}
|
||||
allConversationsForBatch = await fetchAllConversations('');
|
||||
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
updateBatchManageTitle(allConversationsForBatch.length);
|
||||
|
||||
+218
-20
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
let projectsCache = [];
|
||||
let projectsCacheAll = [];
|
||||
const PROJECTS_LIST_PAGE_SIZE_KEY = 'cyberstrike.projects_list_page_size';
|
||||
let currentProjectId = null;
|
||||
let currentProjectTab = 'facts';
|
||||
const projectNameById = {};
|
||||
@@ -167,23 +168,128 @@ function rebuildProjectNameMap(list) {
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchProjectsList(includeArchived) {
|
||||
function getProjectsListPageSize() {
|
||||
try {
|
||||
const saved = parseInt(localStorage.getItem(PROJECTS_LIST_PAGE_SIZE_KEY), 10);
|
||||
if ([20, 50, 100].includes(saved)) return saved;
|
||||
} catch (e) { /* ignore */ }
|
||||
return 50;
|
||||
}
|
||||
|
||||
let projectsListPagination = { page: 1, pageSize: getProjectsListPageSize(), total: 0 };
|
||||
let projectsListSearch = '';
|
||||
let _projectsListSearchDebounce = null;
|
||||
|
||||
function parseListTotalValue(raw, itemsLength) {
|
||||
if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw;
|
||||
if (raw != null && raw !== '') {
|
||||
const n = parseInt(String(raw), 10);
|
||||
if (Number.isFinite(n) && n >= 0) return n;
|
||||
}
|
||||
return itemsLength;
|
||||
}
|
||||
|
||||
function parseListOffsetValue(raw) {
|
||||
if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw;
|
||||
if (raw != null && raw !== '') {
|
||||
const n = parseInt(String(raw), 10);
|
||||
if (Number.isFinite(n) && n >= 0) return n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function parseProjectsListResponse(data) {
|
||||
if (Array.isArray(data)) {
|
||||
return { items: data, total: data.length, limit: data.length, offset: 0, isLegacyArray: true };
|
||||
}
|
||||
const items = data.projects || data.items || [];
|
||||
const arr = Array.isArray(items) ? items : [];
|
||||
return {
|
||||
items: arr,
|
||||
total: parseListTotalValue(data.total, arr.length),
|
||||
limit: parseListTotalValue(data.limit, arr.length) || arr.length,
|
||||
offset: parseListOffsetValue(data.offset),
|
||||
isLegacyArray: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveProjectsListTotal(params, parsed, pageSize, offset) {
|
||||
const serverTotal = parsed.total;
|
||||
// 服务端 total 明确大于当前页末尾 → 直接信任
|
||||
if (!parsed.isLegacyArray && serverTotal > offset + parsed.items.length) {
|
||||
return serverTotal;
|
||||
}
|
||||
// 不足一页 → 已是最后一页
|
||||
if (parsed.items.length < pageSize) {
|
||||
return Math.max(serverTotal, offset + parsed.items.length);
|
||||
}
|
||||
// 满页但 total 可能被误算为 items.length → 探测下一页
|
||||
const probe = new URLSearchParams(params);
|
||||
probe.set('offset', String(offset + pageSize));
|
||||
probe.set('limit', '1');
|
||||
try {
|
||||
const res = await apiFetch(`/api/projects?${probe}`);
|
||||
if (!res.ok) return Math.max(serverTotal, offset + parsed.items.length);
|
||||
const probeParsed = parseProjectsListResponse(await res.json());
|
||||
if (probeParsed.total > serverTotal) return probeParsed.total;
|
||||
if (probeParsed.items.length > 0) {
|
||||
return Math.max(serverTotal, offset + pageSize + 1);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return Math.max(serverTotal, offset + parsed.items.length);
|
||||
}
|
||||
|
||||
async function fetchAllProjects(includeArchived) {
|
||||
const showArchived = includeArchived || document.getElementById('projects-show-archived')?.checked;
|
||||
const url = showArchived ? '/api/projects?limit=200' : '/api/projects?status=active&limit=200';
|
||||
const res = await apiFetch(url);
|
||||
let all = [];
|
||||
const pageSize = 200;
|
||||
let offset = 0;
|
||||
let total = Infinity;
|
||||
while (all.length < total) {
|
||||
const params = new URLSearchParams({ limit: String(pageSize), offset: String(offset) });
|
||||
if (!showArchived) params.set('status', 'active');
|
||||
const res = await apiFetch(`/api/projects?${params}`);
|
||||
if (!res.ok) throw new Error(tp('projects.loadProjectsFailed'));
|
||||
const parsed = parseProjectsListResponse(await res.json());
|
||||
all = all.concat(parsed.items);
|
||||
total = parsed.total;
|
||||
if (!parsed.items.length) break;
|
||||
offset += parsed.items.length;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
async function fetchProjectsList(includeArchived, opts = {}) {
|
||||
const showArchived = includeArchived || document.getElementById('projects-show-archived')?.checked;
|
||||
const page = opts.page ?? projectsListPagination.page;
|
||||
const pageSize = opts.pageSize ?? getProjectsListPageSize();
|
||||
const search = opts.search !== undefined ? opts.search : projectsListSearch;
|
||||
projectsListSearch = search;
|
||||
const offset = (page - 1) * pageSize;
|
||||
const params = new URLSearchParams({ limit: String(pageSize), offset: String(offset) });
|
||||
if (search) params.set('search', search);
|
||||
if (!showArchived) params.set('status', 'active');
|
||||
const res = await apiFetch(`/api/projects?${params}`);
|
||||
if (!res.ok) throw new Error(tp('projects.loadProjectsFailed'));
|
||||
const data = await res.json();
|
||||
projectsCache = Array.isArray(data) ? data : [];
|
||||
rebuildProjectNameMap(projectsCache);
|
||||
_projectsListReady = true;
|
||||
const parsed = parseProjectsListResponse(await res.json());
|
||||
const total = await resolveProjectsListTotal(params, parsed, pageSize, offset);
|
||||
projectsCache = parsed.items;
|
||||
projectsListPagination = { page, pageSize: pageSize, total };
|
||||
rebuildProjectNameMap(projectsCacheAll.length ? projectsCacheAll : projectsCache);
|
||||
return projectsCache;
|
||||
}
|
||||
|
||||
/** 对话页等项目选择器:确保列表已拉取(去重并发请求) */
|
||||
/** 对话页等项目选择器:确保全量列表已拉取(去重并发请求) */
|
||||
async function ensureProjectsLoaded(force) {
|
||||
if (!force && _projectsListReady) return projectsCache;
|
||||
if (!force && _projectsListReady) return projectsCacheAll;
|
||||
if (!force && _projectsFetchPromise) return _projectsFetchPromise;
|
||||
_projectsFetchPromise = fetchProjectsList(false)
|
||||
_projectsFetchPromise = fetchAllProjects(false)
|
||||
.then((list) => {
|
||||
projectsCacheAll = list;
|
||||
rebuildProjectNameMap(projectsCacheAll);
|
||||
_projectsListReady = true;
|
||||
return projectsCacheAll;
|
||||
})
|
||||
.catch((e) => {
|
||||
_projectsListReady = false;
|
||||
throw e;
|
||||
@@ -204,9 +310,10 @@ async function ensureDefaultActiveProjectForNewChat() {
|
||||
await ensureProjectsLoaded();
|
||||
const cur = getActiveProjectId();
|
||||
if (cur && isActiveChatProjectId(cur)) return cur;
|
||||
const source = projectsCacheAll.length ? projectsCacheAll : projectsCache;
|
||||
const first =
|
||||
projectsCache.find((p) => p.pinned && p.status !== 'archived') ||
|
||||
projectsCache.find((p) => p.status !== 'archived');
|
||||
source.find((p) => p.pinned && p.status !== 'archived') ||
|
||||
source.find((p) => p.status !== 'archived');
|
||||
if (first) {
|
||||
setActiveProjectId(first.id);
|
||||
return first.id;
|
||||
@@ -238,6 +345,8 @@ async function initProjectsPage() {
|
||||
initProjectsModalEscape();
|
||||
syncProjectsModalBodyLock();
|
||||
updateProjectsDetailVisibility();
|
||||
projectsListPagination.pageSize = getProjectsListPageSize();
|
||||
renderProjectsPagination();
|
||||
await loadProjectsList();
|
||||
if (!currentProjectId && projectsCache.length) {
|
||||
const fromHash = new URLSearchParams(window.location.hash.split('?')[1] || '').get('id');
|
||||
@@ -250,8 +359,19 @@ async function initProjectsPage() {
|
||||
}
|
||||
|
||||
async function loadProjectsList() {
|
||||
_projectsListReady = false;
|
||||
projectsCacheAll = [];
|
||||
projectsListPagination.pageSize = getProjectsListPageSize();
|
||||
await fetchProjectsList();
|
||||
renderProjectsSidebar();
|
||||
renderProjectsPagination();
|
||||
try {
|
||||
projectsCacheAll = await fetchAllProjects();
|
||||
rebuildProjectNameMap(projectsCacheAll);
|
||||
_projectsListReady = true;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
if (typeof refreshChatProjectSelector === 'function') {
|
||||
refreshChatProjectSelector();
|
||||
}
|
||||
@@ -277,7 +397,7 @@ function updateProjectsDetailVisibility() {
|
||||
|
||||
function updateProjectsListCount() {
|
||||
const el = document.getElementById('projects-list-count');
|
||||
if (el) el.textContent = String(projectsCache.length);
|
||||
if (el) el.textContent = String(projectsListPagination.total || projectsCache.length);
|
||||
}
|
||||
|
||||
/** 事实分类 → 徽章样式(与 fact_template.go 常量对齐) */
|
||||
@@ -385,26 +505,97 @@ function getProjectsListFilter() {
|
||||
}
|
||||
|
||||
function filterProjectsList() {
|
||||
renderProjectsSidebar();
|
||||
if (_projectsListSearchDebounce) clearTimeout(_projectsListSearchDebounce);
|
||||
_projectsListSearchDebounce = setTimeout(() => {
|
||||
_projectsListSearchDebounce = null;
|
||||
const q = getProjectsListFilter();
|
||||
projectsListPagination.page = 1;
|
||||
fetchProjectsList(undefined, { page: 1, search: q })
|
||||
.then(() => {
|
||||
renderProjectsSidebar();
|
||||
renderProjectsPagination();
|
||||
})
|
||||
.catch((e) => console.warn(e));
|
||||
}, 280);
|
||||
}
|
||||
|
||||
function goProjectsPage(page) {
|
||||
const totalPages = Math.max(1, Math.ceil((projectsListPagination.total || 0) / projectsListPagination.pageSize) || 1);
|
||||
const next = Math.min(Math.max(1, page), totalPages);
|
||||
if (next === projectsListPagination.page) return;
|
||||
fetchProjectsList(undefined, { page: next })
|
||||
.then(() => {
|
||||
renderProjectsSidebar();
|
||||
renderProjectsPagination();
|
||||
const listEl = document.getElementById('projects-list');
|
||||
if (listEl) listEl.scrollTop = 0;
|
||||
})
|
||||
.catch((e) => console.warn(e));
|
||||
}
|
||||
|
||||
function changeProjectsPageSize() {
|
||||
const sel = document.getElementById('projects-page-size-pagination');
|
||||
const newSize = sel ? parseInt(sel.value, 10) : 50;
|
||||
if (![20, 50, 100].includes(newSize)) return;
|
||||
try {
|
||||
localStorage.setItem(PROJECTS_LIST_PAGE_SIZE_KEY, String(newSize));
|
||||
} catch (e) { /* ignore */ }
|
||||
projectsListPagination.pageSize = newSize;
|
||||
projectsListPagination.page = 1;
|
||||
fetchProjectsList(undefined, { page: 1, pageSize: newSize })
|
||||
.then(() => {
|
||||
renderProjectsSidebar();
|
||||
renderProjectsPagination();
|
||||
})
|
||||
.catch((e) => console.warn(e));
|
||||
}
|
||||
|
||||
function renderProjectsPagination() {
|
||||
const el = document.getElementById('projects-pagination');
|
||||
if (!el) return;
|
||||
const { page, pageSize, total } = projectsListPagination;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize) || 1);
|
||||
const navDisabled = total === 0 || totalPages <= 1;
|
||||
el.hidden = false;
|
||||
const start = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
||||
const end = total === 0 ? 0 : Math.min(page * pageSize, total);
|
||||
const infoText = tpFmt('projects.paginationRange', `${start}-${end}/${total}`, { start, end, total });
|
||||
const pageText = tpFmt('projects.paginationPage', `${page}/${totalPages}`, { page, total: totalPages });
|
||||
el.innerHTML = `
|
||||
<div class="sidebar-list-pagination-inner sidebar-list-pagination-inner--compact">
|
||||
<span class="pagination-info">${escapeHtml(infoText)}</span>
|
||||
<div class="pagination-controls">
|
||||
<button type="button" class="btn-icon-pagination" onclick="goProjectsPage(${page - 1})" ${page <= 1 || navDisabled ? 'disabled' : ''} title="${escapeHtml(tp('projects.paginationPrev'))}" aria-label="${escapeHtml(tp('projects.paginationPrev'))}">‹</button>
|
||||
<span class="pagination-page">${escapeHtml(pageText)}</span>
|
||||
<button type="button" class="btn-icon-pagination" onclick="goProjectsPage(${page + 1})" ${page >= totalPages || navDisabled ? 'disabled' : ''} title="${escapeHtml(tp('projects.paginationNext'))}" aria-label="${escapeHtml(tp('projects.paginationNext'))}">›</button>
|
||||
</div>
|
||||
<label class="pagination-page-size">
|
||||
${escapeHtml(tp('projects.paginationPerPage'))}
|
||||
<select id="projects-page-size-pagination" onchange="changeProjectsPageSize()">
|
||||
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
|
||||
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
|
||||
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderProjectsSidebar() {
|
||||
const el = document.getElementById('projects-list');
|
||||
if (!el) return;
|
||||
updateProjectsListCount();
|
||||
const q = getProjectsListFilter();
|
||||
const list = q
|
||||
? projectsCache.filter((p) => (p.name || '').toLowerCase().includes(q) || (p.description || '').toLowerCase().includes(q))
|
||||
: projectsCache;
|
||||
const list = projectsCache;
|
||||
if (!projectsCache.length) {
|
||||
el.innerHTML =
|
||||
`<div class="projects-empty">${escapeHtml(tp('projects.noProjects'))}<br><button type="button" class="btn-primary btn-small projects-empty-btn" onclick="showNewProjectModal()">${escapeHtml(tp('projects.newProject'))}</button></div>`;
|
||||
updateProjectsDetailVisibility();
|
||||
renderProjectsPagination();
|
||||
return;
|
||||
}
|
||||
if (!list.length) {
|
||||
el.innerHTML = `<div class="projects-empty">${escapeHtml(tp('projects.noMatchingProjects'))}</div>`;
|
||||
updateProjectsDetailVisibility();
|
||||
renderProjectsPagination();
|
||||
return;
|
||||
}
|
||||
el.innerHTML = list.map((p) => {
|
||||
@@ -1345,7 +1536,8 @@ function getChatProjectSelection() {
|
||||
|
||||
function isActiveChatProjectId(id) {
|
||||
if (!id) return false;
|
||||
return projectsCache.some((p) => p.id === id && p.status !== 'archived');
|
||||
const source = projectsCacheAll.length ? projectsCacheAll : projectsCache;
|
||||
return source.some((p) => p.id === id && p.status !== 'archived');
|
||||
}
|
||||
|
||||
/** 用于 UI:无效/已删除/无可用项目时视为未绑定 */
|
||||
@@ -1400,7 +1592,8 @@ function renderChatProjectPanelList() {
|
||||
const list = document.getElementById('chat-project-list');
|
||||
if (!list) return;
|
||||
const selected = resolveChatProjectSelection();
|
||||
const activeProjects = projectsCache.filter((p) => p.status !== 'archived');
|
||||
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>`;
|
||||
@@ -1543,6 +1736,7 @@ function initChatProjectSelector() {
|
||||
window._projectsLanguageListenerBound = true;
|
||||
document.addEventListener('languagechange', () => {
|
||||
renderProjectsSidebar();
|
||||
renderProjectsPagination();
|
||||
updateChatProjectButtonLabel();
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
if (panel && panel.style.display === 'flex') renderChatProjectPanelList();
|
||||
@@ -1602,6 +1796,10 @@ window.restoreProjectFactByKey = restoreProjectFactByKey;
|
||||
window.openVulnerabilitiesForProject = openVulnerabilitiesForProject;
|
||||
window.openVulnerabilityDetail = openVulnerabilityDetail;
|
||||
window.filterProjectsList = filterProjectsList;
|
||||
window.goProjectsPage = goProjectsPage;
|
||||
window.changeProjectsPageSize = changeProjectsPageSize;
|
||||
window.parseProjectsListResponse = parseProjectsListResponse;
|
||||
window.fetchAllProjects = fetchAllProjects;
|
||||
window.debouncedLoadProjectFacts = debouncedLoadProjectFacts;
|
||||
window.debouncedLoadProjectVulnerabilities = debouncedLoadProjectVulnerabilities;
|
||||
window.loadProjectVulnerabilities = loadProjectVulnerabilities;
|
||||
|
||||
+12
-5
@@ -819,12 +819,19 @@ async function refreshBatchProjectSelectOptions() {
|
||||
projectSelect.innerHTML = `<option value="">${escapeHtml(noneLabel)}</option>`;
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/api/projects?status=active&limit=200');
|
||||
if (!response.ok) {
|
||||
throw new Error(_t('projects.loadProjectsFailed'));
|
||||
let list = [];
|
||||
if (typeof fetchAllProjects === 'function') {
|
||||
list = await fetchAllProjects(false);
|
||||
} else {
|
||||
const response = await apiFetch('/api/projects?status=active&limit=500');
|
||||
if (!response.ok) {
|
||||
throw new Error(_t('projects.loadProjectsFailed'));
|
||||
}
|
||||
const data = await response.json();
|
||||
list = typeof parseProjectsListResponse === 'function'
|
||||
? parseProjectsListResponse(data).items
|
||||
: (Array.isArray(data) ? data : (data.projects || []));
|
||||
}
|
||||
const projects = await response.json();
|
||||
const list = Array.isArray(projects) ? projects : [];
|
||||
const activeProjectId = typeof getActiveProjectId === 'function' ? getActiveProjectId() || '' : '';
|
||||
|
||||
list.forEach((project) => {
|
||||
|
||||
@@ -855,11 +855,6 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(listContainer);
|
||||
}
|
||||
// 清空分页信息
|
||||
const paginationContainer = document.getElementById('vulnerability-pagination');
|
||||
if (paginationContainer) {
|
||||
paginationContainer.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -960,12 +955,6 @@ function renderVulnerabilityPagination() {
|
||||
|
||||
const { currentPage, totalPages, total, pageSize } = vulnerabilityPagination;
|
||||
|
||||
// 如果没有数据,不显示分页控件
|
||||
if (total === 0) {
|
||||
paginationContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算显示范围
|
||||
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
|
||||
const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total);
|
||||
@@ -1052,13 +1041,23 @@ async function populateVulnerabilityModalProjectSelect(selectedId) {
|
||||
const sel = document.getElementById('vulnerability-project-id');
|
||||
if (!sel) return;
|
||||
try {
|
||||
const res = await apiFetch('/api/projects?limit=200');
|
||||
if (res.ok) {
|
||||
const list = await res.json();
|
||||
let list = [];
|
||||
if (typeof fetchAllProjects === 'function') {
|
||||
list = await fetchAllProjects();
|
||||
} else {
|
||||
const res = await apiFetch('/api/projects?limit=500');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
list = typeof parseProjectsListResponse === 'function'
|
||||
? parseProjectsListResponse(data).items
|
||||
: (Array.isArray(data) ? data : (data.projects || []));
|
||||
}
|
||||
}
|
||||
if (list.length) {
|
||||
if (typeof rebuildProjectNameMap === 'function') {
|
||||
rebuildProjectNameMap(list);
|
||||
} else if (typeof projectNameById !== 'undefined') {
|
||||
(list || []).forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
|
||||
list.forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1722,9 +1721,17 @@ async function refreshVulnerabilityProjectFilter() {
|
||||
const sel = document.getElementById('vulnerability-project-filter');
|
||||
if (!sel) return;
|
||||
try {
|
||||
const res = await apiFetch('/api/projects?limit=200');
|
||||
if (!res.ok) return;
|
||||
const list = await res.json();
|
||||
let list = [];
|
||||
if (typeof fetchAllProjects === 'function') {
|
||||
list = await fetchAllProjects(true);
|
||||
} else {
|
||||
const res = await apiFetch('/api/projects?limit=500');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
list = typeof parseProjectsListResponse === 'function'
|
||||
? parseProjectsListResponse(data).items
|
||||
: (Array.isArray(data) ? data : (data.projects || []));
|
||||
}
|
||||
if (typeof rebuildProjectNameMap === 'function') {
|
||||
rebuildProjectNameMap(list);
|
||||
} else if (typeof projectNameById !== 'undefined') {
|
||||
|
||||
@@ -826,6 +826,7 @@
|
||||
<div id="conversations-list" class="conversations-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="conversations-pagination" class="sidebar-list-pagination conversation-sidebar-pagination"></div>
|
||||
<div id="chat-reasoning-wrapper" class="chat-reasoning-wrapper conversation-reasoning-card conversation-reasoning-collapsed" style="display: none;">
|
||||
<button type="button" id="conversation-reasoning-toggle" class="conversation-reasoning-card-header" onclick="toggleConversationReasoningCard()" aria-expanded="false" aria-controls="conversation-reasoning-body" data-i18n="chat.reasoningCompactAria" data-i18n-attr="aria-label,title" data-i18n-skip-text="true" aria-label="模型推理选项" title="模型推理选项">
|
||||
<div class="conversation-reasoning-heading">
|
||||
@@ -1464,6 +1465,7 @@
|
||||
<input type="search" id="projects-list-search" class="form-input" placeholder="搜索项目…" oninput="filterProjectsList()" autocomplete="off" data-i18n="projects.searchProjectsPlaceholder" data-i18n-attr="placeholder">
|
||||
</div>
|
||||
<div id="projects-list" class="projects-list"></div>
|
||||
<div id="projects-pagination" class="sidebar-list-pagination projects-sidebar-pagination"></div>
|
||||
</aside>
|
||||
<main class="projects-detail" id="projects-detail-main">
|
||||
<div class="projects-detail-placeholder" id="projects-detail-placeholder">
|
||||
|
||||
Reference in New Issue
Block a user