From 1c576db75d6dfcc012fd18a8cd15ee024535daec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:55:55 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 760 ++++++++++++++++++++++++++++++++++--- web/static/i18n/en-US.json | 4 + web/static/i18n/zh-CN.json | 4 + web/static/js/chat.js | 312 ++++++++++++++- web/static/js/monitor.js | 107 +++++- web/static/js/skills.js | 93 +++-- web/templates/index.html | 22 +- 7 files changed, 1188 insertions(+), 114 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index ae87311f..234fa31f 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -1272,32 +1272,59 @@ html[data-theme="dark"] .openapi-doc-btn:hover { .new-chat-btn { width: 100%; - padding: 10px 16px; - background: var(--accent-color); - color: white; - border: none; + min-height: 40px; + padding: 9px 16px; + background: linear-gradient(180deg, #60a5fa 0%, #3b82f6 100%); + color: #ffffff; + border: 1px solid rgba(96, 165, 250, 0.72); border-radius: 8px; font-size: 0.9375rem; - font-weight: 500; + font-weight: 650; cursor: pointer; - transition: all 0.2s; + transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; display: flex; align-items: center; justify-content: center; - gap: 8px; + gap: 7px; + position: relative; + overflow: hidden; + letter-spacing: 0; + box-shadow: 0 6px 14px rgba(37, 99, 235, 0.18); } .new-chat-btn:hover { - background: var(--accent-hover); - transform: translateY(-1px); - box-shadow: var(--shadow-sm); + background: linear-gradient(180deg, #74b2fb 0%, #4b8df7 100%); + border-color: rgba(147, 197, 253, 0.9); + box-shadow: 0 8px 16px rgba(37, 99, 235, 0.22); +} + +.new-chat-btn:active { + background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); + box-shadow: 0 3px 8px rgba(37, 99, 235, 0.16); +} + +.new-chat-btn:focus-visible { + outline: none; + box-shadow: + 0 0 0 3px rgba(96, 165, 250, 0.22), + 0 6px 14px rgba(37, 99, 235, 0.18); } .new-chat-btn span { - font-size: 1.2em; line-height: 1; } +.new-chat-btn span:first-child { + flex-shrink: 0; + font-size: 1.125rem; + font-weight: 700; + margin-top: -1px; +} + +.new-chat-btn span:last-child { + white-space: nowrap; +} + .sidebar-content { flex: 1; overflow-y: auto; @@ -5027,6 +5054,12 @@ html[data-theme="dark"] .openapi-doc-btn:hover { background: var(--bg-tertiary); } +.timeline-item.timeline-item-focus-pulse { + outline: 2px solid rgba(96, 165, 250, 0.85); + outline-offset: 2px; + animation: highlight-flash 1.1s ease-in-out 2; +} + /* 迭代轮次:暖琥珀色条 + 极浅底,与紫(推理)/蓝(工具)区分但不抢视觉 */ .timeline-item-iteration { border-left-color: #c4a574; @@ -5061,7 +5094,15 @@ html[data-theme="dark"] .openapi-doc-btn:hover { /* 迭代中主通道流式正文(标题常为「助手输出」等):中性底 + 主色条,表示对用户可见的答复流 */ .timeline-item-thinking[data-response-stream-placeholder="1"] { border-left-color: var(--accent-color); - background: rgba(0, 102, 255, 0.04); + background: rgba(59, 130, 246, 0.055); + border-color: rgba(59, 130, 246, 0.16); +} + +/* Eino single/deep/supervisor 的主通道是助手正文,不使用 planning 的青绿色卡片 */ +.timeline-item-planning[data-orchestration]:not([data-orchestration="plan_execute"]) { + border-left-color: #3b82f6; + background: rgba(59, 130, 246, 0.055); + border-color: rgba(59, 130, 246, 0.16); } .timeline-item-reasoning_chain { @@ -5559,49 +5600,12 @@ html[data-theme="dark"] .openapi-doc-btn:hover { .timeline-item.tool-detail-collapsible .timeline-item-header { cursor: pointer; position: relative; - padding-right: 104px; } .timeline-item.tool-detail-collapsible .timeline-item-header:hover .timeline-item-title { color: var(--accent-color); } -.timeline-item.tool-detail-collapsible .timeline-item-header::after { - content: attr(data-tool-detail-label); - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - padding: 3px 8px; - border-radius: 999px; - border: 1px solid rgba(96, 165, 250, 0.35); - color: #93c5fd; - background: rgba(37, 99, 235, 0.12); - font-size: 0.75rem; - font-weight: 600; - line-height: 1.2; - white-space: nowrap; -} - -.timeline-item.tool-detail-collapsible .timeline-item-header:hover::after { - color: #dbeafe; - border-color: rgba(147, 197, 253, 0.65); - background: rgba(37, 99, 235, 0.22); -} - -@media (max-width: 640px) { - .timeline-item.tool-detail-collapsible .timeline-item-header { - padding-right: 0; - } - - .timeline-item.tool-detail-collapsible .timeline-item-header::after { - position: static; - transform: none; - margin-left: auto; - flex: 0 0 auto; - } -} - /* 参数块与卡片类型色弱对齐,扫读时一眼归到「调用」 */ .timeline-item-tool_call .tool-args { background: rgba(21, 101, 192, 0.06); @@ -24978,6 +24982,313 @@ html[data-theme="dark"] .vulnerability-details code.vuln-detail-field-value { } } +.skills-management-page .page-content { + padding: 18px 22px 0; + gap: 14px; +} + +.skills-overview { + display: grid; + grid-template-columns: minmax(160px, 1.2fr) repeat(3, minmax(110px, 0.55fr)); + gap: 12px; +} + +.skills-overview-card { + min-height: 76px; + padding: 14px 16px; + border: 1px solid var(--border-color); + border-radius: 10px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.58)), + var(--bg-primary); + box-shadow: 0 8px 22px rgba(15, 23, 42, 0.06); + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: hidden; +} + +.skills-overview-card-primary { + border-color: rgba(0, 102, 255, 0.28); + background: + linear-gradient(135deg, rgba(0, 102, 255, 0.16), rgba(40, 167, 69, 0.10)), + var(--bg-primary); +} + +.skills-overview-label { + font-size: 0.75rem; + line-height: 1.2; + color: var(--text-secondary); + font-weight: 600; +} + +.skills-overview-card strong { + font-size: 1.55rem; + line-height: 1; + color: var(--text-primary); + font-weight: 700; +} + +.skills-management-page .skills-controls { + margin-bottom: 0; +} + +.skills-management-page .skills-search-box { + margin-bottom: 0; +} + +.skills-management-page .skills-search-box::before { + content: ""; + position: absolute; + left: 16px; + top: 50%; + width: 16px; + height: 16px; + transform: translateY(-50%); + opacity: 0.58; + background: currentColor; + color: var(--text-secondary); + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21 21-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3C/svg%3E") center / contain no-repeat; +} + +.skills-management-page .skills-search-box input { + height: 44px; + padding: 10px 44px 10px 44px; + border-radius: 10px; + background: var(--bg-primary); + box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04); +} + +.skills-management-page .skills-grid { + display: flex; + flex-direction: column; + gap: 10px; + padding: 2px 2px 18px; +} + +.skills-management-page .skill-card { + min-height: 92px; + display: grid; + grid-template-columns: 46px minmax(0, 1fr); + align-items: center; + gap: 14px 16px; + padding: 14px 18px; + border-radius: 10px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 249, 250, 0.88)), + var(--bg-primary); + box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06); + position: relative; + overflow: hidden; +} + +.skills-management-page .skill-card::after { + content: ""; + position: absolute; + inset: 0; + border-top: 2px solid rgba(0, 102, 255, 0.38); + opacity: 0; + transition: opacity 0.2s; + pointer-events: none; +} + +.skills-management-page .skill-card:hover { + transform: translateY(-3px); + border-color: rgba(0, 102, 255, 0.38); + box-shadow: 0 16px 34px rgba(15, 23, 42, 0.12); +} + +.skills-management-page .skill-card:hover::after { + opacity: 1; +} + +.skill-card-mark { + width: 46px; + height: 46px; + border-radius: 10px; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(0, 102, 255, 0.18), rgba(40, 167, 69, 0.14)); + color: var(--accent-color); + font-size: 1rem; + font-weight: 800; + border: 1px solid rgba(0, 102, 255, 0.18); +} + +.skill-card-body { + min-width: 0; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px 18px; +} + +.skills-management-page .skill-card-header { + min-width: 0; + gap: 8px; +} + +.skill-card-title-row { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.skills-management-page .skill-card-title { + font-size: 1.02rem; + line-height: 1.35; + word-break: normal; + overflow-wrap: anywhere; + flex: 0 1 auto; +} + +.skill-card-meta { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.skill-card-meta span { + max-width: 100%; + padding: 3px 8px; + border-radius: 999px; + background: rgba(0, 102, 255, 0.08); + color: var(--accent-color); + border: 1px solid rgba(0, 102, 255, 0.14); + font-size: 0.72rem; + line-height: 1.25; + font-weight: 600; +} + +.skills-management-page .skill-card-description { + color: var(--text-secondary); + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + min-height: 1.4em; +} + +.skills-management-page .skill-card-actions { + grid-column: 2; + grid-row: 1; + margin-top: 0; + align-items: center; + justify-content: flex-end; + flex-wrap: nowrap; + align-self: center; +} + +.skills-management-page .skill-card-actions .btn-secondary { + padding: 6px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.66); +} + +.skills-management-page .skill-card-actions .btn-danger { + color: var(--error-color); +} + +html[data-theme="dark"] .skills-overview-card, +html[data-theme="dark"] .skills-management-page .skill-card, +html[data-theme="dark"] .skills-management-page .skills-search-box input { + background: + linear-gradient(180deg, rgba(17, 24, 39, 0.96), rgba(15, 23, 42, 0.92)), + #111827; + border-color: #263244; + box-shadow: 0 16px 34px rgba(0, 0, 0, 0.20); +} + +html[data-theme="dark"] .skills-overview-card-primary { + border-color: rgba(96, 165, 250, 0.30); + background: + linear-gradient(135deg, rgba(96, 165, 250, 0.14), rgba(52, 211, 153, 0.08)), + #111827; +} + +html[data-theme="dark"] .skill-card-mark { + background: linear-gradient(135deg, rgba(96, 165, 250, 0.18), rgba(52, 211, 153, 0.12)); + border-color: rgba(96, 165, 250, 0.22); +} + +html[data-theme="dark"] .skill-card-meta span { + background: rgba(96, 165, 250, 0.12); + border-color: rgba(96, 165, 250, 0.22); +} + +html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-secondary { + background: rgba(15, 23, 42, 0.72); +} + +@media (max-width: 1100px) { + .skills-overview { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 768px) { + .skills-management-page .page-content { + padding: 14px 14px 0; + } + + .skills-overview { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + + .skills-overview-card { + min-height: 68px; + padding: 12px; + } + + .skills-overview-card strong { + font-size: 1.25rem; + } + + .skills-management-page .skill-card { + min-height: auto; + align-items: flex-start; + } + + .skill-card-body { + grid-template-columns: 1fr; + } + + .skills-management-page .skill-card-actions { + grid-column: 1; + grid-row: auto; + width: 100%; + margin-left: 0; + justify-content: flex-start; + flex-wrap: wrap; + } +} + +@media (max-width: 520px) { + .skills-overview { + grid-template-columns: 1fr; + } + + .skills-management-page .skill-card { + grid-template-columns: 1fr; + } + + .skill-card-title-row { + align-items: flex-start; + flex-direction: column; + gap: 7px; + } + + .skill-card-mark { + width: 40px; + height: 40px; + } +} + .skill-item { background: var(--bg-primary); border: 1px solid var(--border-color); @@ -25135,6 +25446,323 @@ html[data-theme="dark"] .vulnerability-details code.vuln-detail-field-value { color: var(--text-secondary); } +/* Skills 状态监控:专属视觉增强 */ +.skills-monitor-page { + --skills-blue: #2563eb; + --skills-cyan: #06b6d4; + --skills-success: #16a34a; + --skills-danger: #e11d48; + --skills-card-shadow: 0 10px 28px rgba(15, 23, 42, 0.08); +} + +.skills-monitor-page .page-content { + background: + linear-gradient(180deg, rgba(37, 99, 235, 0.06) 0%, rgba(248, 250, 252, 0) 260px), + #f5f7fb; + padding: 20px 24px 28px; +} + +.skills-monitor-page .page-header { + padding: 16px 24px; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(12px); +} + +.skills-monitor-page .page-header h2 { + font-weight: 750; +} + +.skills-monitor-page .monitor-sections { + gap: 16px; +} + +.skills-monitor-page .monitor-section { + border-color: rgba(37, 99, 235, 0.1); + border-radius: 14px; + box-shadow: var(--skills-card-shadow); +} + +.skills-monitor-page .monitor-section.monitor-overview { + background: transparent; + border: 0; + box-shadow: none; +} + +.skills-monitor-page .monitor-section .section-header h3 { + display: inline-flex; + align-items: center; + gap: 10px; + font-size: 1.05rem; +} + +.skills-monitor-page .monitor-section .section-header { + background: transparent; + border: 0; + box-shadow: none; + padding: 0; +} + +.skills-monitor-page .monitor-section .section-header h3::before { + content: ""; + width: 9px; + height: 9px; + border-radius: 999px; + background: linear-gradient(135deg, var(--skills-blue), var(--skills-cyan)); + box-shadow: 0 0 0 5px rgba(37, 99, 235, 0.1); +} + +.skills-monitor-page .monitor-stats-grid { + grid-template-columns: repeat(5, minmax(150px, 1fr)); + gap: 18px; +} + +.skills-monitor-page .skills-stat-card { + position: relative; + isolation: isolate; + min-height: 124px; + padding: 20px; + border: 1px solid rgba(37, 99, 235, 0.12); + border-radius: 16px; + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.92)), + radial-gradient(circle at 100% 0%, rgba(37, 99, 235, 0.14), transparent 34%); + box-shadow: var(--skills-card-shadow); + overflow: hidden; + transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; +} + +.skills-monitor-page .skills-stat-card:hover { + transform: translateY(-2px); + border-color: rgba(37, 99, 235, 0.24); + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.12); +} + +.skills-monitor-page .monitor-stat-label { + color: #64748b; + font-size: 0.86rem; + font-weight: 650; +} + +.skills-monitor-page .monitor-stat-value { + margin-top: 6px; + font-size: 2.15rem; + font-weight: 780; + line-height: 1.05; + letter-spacing: 0; + color: #111827; + font-variant-numeric: tabular-nums; +} + +.skills-monitor-page .monitor-stat-value.is-success, +.skills-monitor-page .skills-table-number.is-success { + color: var(--skills-success); +} + +.skills-monitor-page .monitor-stat-value.is-danger, +.skills-monitor-page .skills-table-number.is-danger { + color: var(--skills-danger); +} + +.skills-monitor-page #skills-monitor-list { + border-color: rgba(15, 23, 42, 0.08); + border-radius: 14px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7); +} + +.skills-monitor-page .skills-monitor-table { + min-width: 900px; + font-size: 0.875rem; +} + +.skills-monitor-page .skills-monitor-table thead { + background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); +} + +.skills-monitor-page .skills-monitor-table th { + position: sticky; + top: 0; + z-index: 1; + padding: 14px 16px; + color: #64748b; + font-size: 0.78rem; + letter-spacing: 0; +} + +.skills-monitor-page .skills-monitor-table th:first-child, +.skills-monitor-page .skills-monitor-table td:first-child { + text-align: left; +} + +.skills-monitor-page .skills-monitor-table th:not(:first-child), +.skills-monitor-page .skills-monitor-table td:not(:first-child) { + text-align: center; +} + +.skills-monitor-page .skills-monitor-table th:last-child, +.skills-monitor-page .skills-monitor-table td:last-child { + text-align: left; +} + +.skills-monitor-page .skills-monitor-table td { + padding: 14px 16px; + border-color: rgba(15, 23, 42, 0.06); + white-space: nowrap; +} + +.skills-monitor-page .skills-monitor-table td:first-child { + color: #1f2937; +} + +.skills-monitor-page .skills-monitor-table tbody tr { + transition: background 0.15s ease, box-shadow 0.15s ease; +} + +.skills-monitor-page .skills-monitor-table tbody tr:hover { + background: rgba(37, 99, 235, 0.055); + box-shadow: inset 3px 0 0 rgba(37, 99, 235, 0.56); +} + +.skills-monitor-page .skills-table-pill, +.skills-monitor-page .skills-table-number { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 34px; + height: 26px; + padding: 0 10px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.05); + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.skills-monitor-page .skills-table-number.is-success { + background: rgba(22, 163, 74, 0.1); +} + +.skills-monitor-page .skills-table-number.is-danger { + background: rgba(225, 29, 72, 0.1); +} + +.skills-monitor-page .skills-rate-cell { + display: inline-grid; + grid-template-columns: 52px 76px; + align-items: center; + gap: 10px; + color: var(--text-primary); + font-weight: 650; + font-variant-numeric: tabular-nums; +} + +.skills-monitor-page .skills-rate-track { + display: block; + height: 7px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.08); + overflow: hidden; +} + +.skills-monitor-page .skills-rate-track span { + display: block; + height: 100%; + min-width: 3px; + border-radius: inherit; + background: linear-gradient(90deg, var(--skills-blue), var(--skills-success)); +} + +.skills-monitor-page .skills-last-call { + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} + +.skills-monitor-page .monitor-empty { + border: 1px dashed rgba(37, 99, 235, 0.18); + border-radius: 14px; + background: rgba(37, 99, 235, 0.035); +} + +@media (max-width: 1280px) { + .skills-monitor-page .monitor-stats-grid { + grid-template-columns: repeat(3, minmax(180px, 1fr)); + } +} + +@media (max-width: 760px) { + .skills-monitor-page .page-content { + padding: 16px; + } + + .skills-monitor-page .monitor-stats-grid { + grid-template-columns: 1fr; + } +} + +html[data-theme="dark"] .skills-monitor-page { + --skills-blue: #60a5fa; + --skills-cyan: #22d3ee; + --skills-success: #34d399; + --skills-danger: #fb7185; + --skills-card-shadow: 0 14px 34px rgba(0, 0, 0, 0.34); +} + +html[data-theme="dark"] .skills-monitor-page .page-content { + background: + linear-gradient(180deg, rgba(37, 99, 235, 0.16) 0%, rgba(11, 17, 32, 0) 280px), + var(--bg-secondary); +} + +html[data-theme="dark"] .skills-monitor-page .page-header { + background: rgba(17, 24, 39, 0.88); +} + +html[data-theme="dark"] .skills-monitor-page .monitor-section { + border-color: rgba(96, 165, 250, 0.18); +} + +html[data-theme="dark"] .skills-monitor-page .monitor-section .section-header { + background: transparent; + border-color: transparent; +} + +html[data-theme="dark"] .skills-monitor-page .skills-stat-card { + border-color: rgba(96, 165, 250, 0.2); + background: + linear-gradient(145deg, rgba(17, 24, 39, 0.96), rgba(15, 23, 42, 0.94)), + radial-gradient(circle at 100% 0%, rgba(96, 165, 250, 0.2), transparent 34%); +} + +html[data-theme="dark"] .skills-monitor-page .monitor-stat-label, +html[data-theme="dark"] .skills-monitor-page .skills-monitor-table th { + color: #94a3b8; +} + +html[data-theme="dark"] .skills-monitor-page .monitor-stat-value, +html[data-theme="dark"] .skills-monitor-page .skills-monitor-table td:first-child { + color: var(--text-primary); +} + +html[data-theme="dark"] .skills-monitor-page .skills-monitor-table thead { + background: linear-gradient(180deg, #172033 0%, #111827 100%); +} + +html[data-theme="dark"] .skills-monitor-page #skills-monitor-list, +html[data-theme="dark"] .skills-monitor-page .skills-monitor-table td { + border-color: rgba(148, 163, 184, 0.16); +} + +html[data-theme="dark"] .skills-monitor-page .skills-table-pill { + background: rgba(148, 163, 184, 0.12); +} + +html[data-theme="dark"] .skills-monitor-page .skills-rate-track { + background: rgba(148, 163, 184, 0.16); +} + +html[data-theme="dark"] .skills-monitor-page .monitor-empty { + border-color: rgba(96, 165, 250, 0.22); + background: rgba(96, 165, 250, 0.08); +} + /* ==================== 信息收集(FOFA)页面 ==================== */ .info-collect-panel { background: var(--bg-primary); @@ -29922,7 +30550,9 @@ html[data-theme="dark"] .timeline-item-thinking { } html[data-theme="dark"] .timeline-item-thinking[data-response-stream-placeholder="1"] { - background: rgba(96, 165, 250, 0.075); + border-left-color: #60a5fa; + background: rgba(37, 99, 235, 0.09); + border-color: rgba(96, 165, 250, 0.2); } html[data-theme="dark"] .timeline-item-reasoning_chain { @@ -29933,6 +30563,12 @@ html[data-theme="dark"] .timeline-item-planning { background: rgba(45, 212, 191, 0.075); } +html[data-theme="dark"] .timeline-item-planning[data-orchestration]:not([data-orchestration="plan_execute"]) { + border-left-color: #60a5fa; + background: #101c2f; + border-color: rgba(96, 165, 250, 0.2); +} + html[data-theme="dark"] .timeline-item-tool_call, html[data-theme="dark"] .timeline-item-tool_calls_detected { background: rgba(96, 165, 250, 0.075); @@ -29989,6 +30625,30 @@ html[data-theme="dark"] .timeline-item-tool_call.tool-call-failed:hover { background: rgba(239, 68, 68, 0.16); } +html[data-theme="dark"] .timeline-item-tool_call .tool-args, +html[data-theme="dark"] .timeline-item-tool_call .tool-result, +html[data-theme="dark"] .timeline-item-tool_result .tool-result { + background: rgba(51, 65, 85, 0.52); + border-color: rgba(148, 163, 184, 0.28); + color: #dbe4f0; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035); +} + +html[data-theme="dark"] .timeline-item-tool_call:has(.tool-result-section.success) .tool-args, +html[data-theme="dark"] .timeline-item-tool_call .tool-result-section.success .tool-result, +html[data-theme="dark"] .timeline-item-tool_result:has(.tool-result-section.success) .tool-result { + background: rgba(16, 185, 129, 0.13); + border-color: rgba(52, 211, 153, 0.38); +} + +html[data-theme="dark"] .timeline-item-tool_call:has(.tool-result-section.error) .tool-args, +html[data-theme="dark"] .timeline-item-tool_call .tool-result-section.error .tool-result, +html[data-theme="dark"] .timeline-item-tool_result:has(.tool-result-section.error) .tool-result { + background: rgba(248, 113, 113, 0.13); + border-color: rgba(248, 113, 113, 0.42); + color: #dbe4f0; +} + html[data-theme="dark"] .timeline-item-warning, html[data-theme="dark"] .timeline-item-user_interrupt_continue { background: rgba(251, 191, 36, 0.08); diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 452d5e3f..8509e310 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2176,6 +2176,10 @@ "clearStatsTitle": "Clear all statistics", "skillsCallStats": "Skills call stats", "searchPlaceholder": "Search Skills...", + "overviewTotal": "Skills", + "overviewCurrent": "This page", + "overviewFiles": "Files", + "overviewScripts": "Scripts", "loading": "Loading...", "paginationShow": "Show {{start}}-{{end}} of {{total}}", "perPageLabel": "Per page", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 15c9b240..2d98ae4a 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2164,6 +2164,10 @@ "clearStatsTitle": "清空所有统计数据", "skillsCallStats": "Skills调用统计", "searchPlaceholder": "搜索Skills...", + "overviewTotal": "Skills", + "overviewCurrent": "当前页", + "overviewFiles": "文件", + "overviewScripts": "脚本", "loading": "加载中...", "paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 条", "perPageLabel": "每页显示", diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 7c8d0c6e..01e8c525 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2463,6 +2463,7 @@ function syncProcessDetailButtonLabels(messageId, expanded) { function renderProcessDetails(messageId, processDetails, options) { const renderOpts = options || {}; const appendMode = !!renderOpts.append; + const prependMode = !!renderOpts.prepend; const markLoaded = renderOpts.markLoaded !== false; const messageElement = document.getElementById(messageId); if (!messageElement) { @@ -2546,13 +2547,21 @@ function renderProcessDetails(messageId, processDetails, options) { processDetails = mergeMessageReasoningContentIntoProcessDetails(processDetails, reasoningFromMessage); processDetails = filterNoiseProcessDetails(processDetails); processDetails = dedupeConsecutiveProcessDetailRows(processDetails); + const renderedMcpIds = collectMcpExecutionIdsFromProcessDetails(processDetails); + if (renderedMcpIds.length > 0) { + setPendingMcpExecutionIds(messageElement, renderedMcpIds); + } + const renderedToolCount = processDetails.filter((d) => d && d.eventType === 'tool_call').length; + if (renderedToolCount > 0) { + setMcpExecutionSummaryCount(messageElement, renderedToolCount); + } if (typeof window.coalesceProcessDetailsToolPairs === 'function') { processDetails = window.coalesceProcessDetailsToolPairs(processDetails); } processDetails = compactWorkflowProcessDetails(processDetails); // 如果没有processDetails或为空,显示空状态 if (!processDetails || processDetails.length === 0) { - if (!appendMode) { + if (!appendMode && !prependMode) { timeline.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '
'; if (!isProcessDetailsUserExpanded(messageId)) { timeline.classList.remove('expanded'); @@ -2561,7 +2570,13 @@ function renderProcessDetails(messageId, processDetails, options) { return; } - if (!appendMode) { + const prependAnchor = prependMode ? timeline.firstChild : null; + const prependScrollBox = prependMode ? document.getElementById('chat-messages') : null; + const prependScrollHeight = prependScrollBox ? prependScrollBox.scrollHeight : 0; + const prependScrollTop = prependScrollBox ? prependScrollBox.scrollTop : 0; + const prependedIds = []; + + if (!appendMode && !prependMode) { timeline.innerHTML = ''; } @@ -2713,7 +2728,24 @@ function renderProcessDetails(messageId, processDetails, options) { if (eventType === 'tool_call' && data._mergedResult) { timelineOpts.mergedResult = data._mergedResult; } - addTimelineItem(timeline, eventType, timelineOpts); + const itemId = addTimelineItem(timeline, eventType, timelineOpts); + if (prependMode && itemId) { + prependedIds.push(itemId); + } + } + + function finishPrependRender() { + if (!prependMode || prependedIds.length === 0) return; + const fragment = document.createDocumentFragment(); + prependedIds.forEach((id) => { + const node = document.getElementById(id); + if (node) fragment.appendChild(node); + }); + timeline.insertBefore(fragment, prependAnchor || timeline.firstChild); + if (prependScrollBox) { + const delta = prependScrollBox.scrollHeight - prependScrollHeight; + prependScrollBox.scrollTop = prependScrollTop + delta; + } } const TIMELINE_RENDER_BATCH = 40; @@ -2725,13 +2757,17 @@ function renderProcessDetails(messageId, processDetails, options) { if (endIdx < processDetails.length) { requestAnimationFrame(() => renderTimelineBatch(endIdx)); } else if (markLoaded) { + finishPrependRender(); finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoaded, timeline); + } else { + finishPrependRender(); } }; if (processDetails.length > TIMELINE_RENDER_BATCH) { renderTimelineBatch(0); } else { processDetails.forEach(renderOneProcessDetail); + finishPrependRender(); if (markLoaded) { finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoaded, timeline); } @@ -2781,6 +2817,18 @@ function prefetchProcessDetailsSummaryHint(messageId, messageElement) { const j = await res.json().catch(() => ({})); if (!res.ok || !j.summary) return; const s = j.summary; + const toolCount = parseInt(s.toolCount, 10) || 0; + const summaryMcpIds = Array.isArray(s.mcpExecutionIds) ? s.mcpExecutionIds : []; + const summaryTools = Array.isArray(s.toolExecutions) ? s.toolExecutions : []; + if (summaryTools.length > 0) { + setPendingToolExecutionSummaries(messageElement, summaryTools); + } + if (summaryMcpIds.length > 0) { + setPendingMcpExecutionIds(messageElement, summaryMcpIds); + } + if (toolCount > 0) { + setMcpExecutionSummaryCount(messageElement, toolCount); + } const timeline = detailsContainer.querySelector('.progress-timeline'); if (!timeline || detailsContainer.dataset.loaded === '1') return; const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; @@ -2871,16 +2919,114 @@ function getPendingMcpExecutionCount(messageElement) { } } +function getPendingToolExecutionSummaryCount(messageElement) { + if (!messageElement || !messageElement.dataset || !messageElement.dataset.pendingToolExecutionSummaries) { + return 0; + } + try { + const tools = JSON.parse(messageElement.dataset.pendingToolExecutionSummaries); + return Array.isArray(tools) ? tools.length : 0; + } catch (e) { + return 0; + } +} + function getMcpExecutionCount(messageElement) { const pending = getPendingMcpExecutionCount(messageElement); if (pending > 0) return pending; + const pendingSummaries = getPendingToolExecutionSummaryCount(messageElement); + if (pendingSummaries > 0) return pendingSummaries; const toolList = messageElement && messageElement.querySelector('.mcp-tool-list'); if (toolList) { - return toolList.querySelectorAll('.mcp-detail-btn[data-exec-id]').length; + const rendered = toolList.querySelectorAll('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]').length; + if (rendered > 0) return rendered; + } + if (messageElement && messageElement.dataset && messageElement.dataset.mcpExecutionCount) { + const summaryCount = parseInt(messageElement.dataset.mcpExecutionCount, 10) || 0; + if (summaryCount > 0) return summaryCount; } return 0; } +function collectMcpExecutionIdsFromProcessDetails(processDetails) { + if (!Array.isArray(processDetails)) return []; + const seen = new Set(); + const ids = []; + const add = (value) => { + const id = value == null ? '' : String(value).trim(); + if (!id || seen.has(id)) return; + seen.add(id); + ids.push(id); + }; + processDetails.forEach((detail) => { + const data = detail && detail.data && typeof detail.data === 'object' ? detail.data : null; + if (!data) return; + add(data.executionId); + const merged = data._mergedResult && typeof data._mergedResult === 'object' ? data._mergedResult : null; + if (merged) add(merged.executionId); + }); + return ids; +} + +function setPendingMcpExecutionIds(messageElement, executionIds) { + if (!messageElement || !messageElement.dataset || !Array.isArray(executionIds)) return; + const seen = new Set(); + const ids = []; + executionIds.forEach((value) => { + const id = value == null ? '' : String(value).trim(); + if (!id || seen.has(id)) return; + seen.add(id); + ids.push(id); + }); + if (ids.length > 0) { + messageElement.dataset.pendingMcpExecutionIds = JSON.stringify(ids); + } else { + delete messageElement.dataset.pendingMcpExecutionIds; + } + if (typeof syncMcpToolsToggleButton === 'function') { + syncMcpToolsToggleButton(messageElement); + } +} + +function normalizeToolExecutionSummaryForButton(raw) { + const data = raw && typeof raw === 'object' ? raw : {}; + return { + toolName: data.toolName || data.name || '', + status: data.status || '', + executionId: data.executionId || '', + toolCallId: data.toolCallId || '', + processDetailId: data.processDetailId || '' + }; +} + +function setPendingToolExecutionSummaries(messageElement, summaries) { + if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return; + const normalized = summaries + .map(normalizeToolExecutionSummaryForButton) + .filter((item) => item.toolName || item.executionId || item.toolCallId); + if (normalized.length > 0) { + messageElement.dataset.pendingToolExecutionSummaries = JSON.stringify(normalized); + } else { + delete messageElement.dataset.pendingToolExecutionSummaries; + } + if (typeof syncMcpToolsToggleButton === 'function') { + syncMcpToolsToggleButton(messageElement); + } +} + +function setMcpExecutionSummaryCount(messageElement, count) { + if (!messageElement || !messageElement.dataset) return; + const n = parseInt(count, 10) || 0; + if (n > 0) { + messageElement.dataset.mcpExecutionCount = String(n); + } else { + delete messageElement.dataset.mcpExecutionCount; + } + if (typeof syncMcpToolsToggleButton === 'function') { + syncMcpToolsToggleButton(messageElement); + } +} + function formatMcpToolsToggleLabel(count, expanded) { if (expanded) { if (typeof window.t === 'function') { @@ -2986,12 +3132,92 @@ function syncMcpToolsToggleButton(messageElement) { toolsToggle.innerHTML = '' + formatMcpToolsToggleLabel(count, expanded) + ''; } +function cssEscapeValue(value) { + const s = String(value || ''); + if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') { + return CSS.escape(s); + } + return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +async function ensureProcessDetailsForToolFocus(messageElement, anchorId) { + if (!messageElement || !messageElement.id) return null; + const detailsId = 'process-details-' + messageElement.id; + let detailsContainer = document.getElementById(detailsId); + const backendId = messageElement.dataset ? String(messageElement.dataset.backendMessageId || '').trim() : ''; + if (detailsContainer && detailsContainer.dataset && detailsContainer.dataset.lazyNotLoaded === '1' && backendId && typeof window.loadProcessDetailsPaginated === 'function') { + await window.loadProcessDetailsPaginated(messageElement.id, backendId, { autoLoadAll: false, anchorId: anchorId || '' }); + detailsContainer = document.getElementById(detailsId); + } else if (!detailsContainer && backendId && typeof renderProcessDetails === 'function') { + renderProcessDetails(messageElement.id, null); + detailsContainer = document.getElementById(detailsId); + if (detailsContainer && typeof window.loadProcessDetailsPaginated === 'function') { + await window.loadProcessDetailsPaginated(messageElement.id, backendId, { autoLoadAll: false, anchorId: anchorId || '' }); + detailsContainer = document.getElementById(detailsId); + } + } + if (typeof window.expandProcessDetailsTimeline === 'function') { + window.expandProcessDetailsTimeline(messageElement.id); + } else if (typeof toggleProcessDetails === 'function') { + const timeline = detailsContainer && detailsContainer.querySelector('.progress-timeline'); + if (!timeline || !timeline.classList.contains('expanded')) { + toggleProcessDetails(null, messageElement.id); + } + } + return document.getElementById(detailsId); +} + +async function focusToolExecutionInProcessDetails(messageElement, summary, index) { + const item = normalizeToolExecutionSummaryForButton(summary); + let detailsContainer = await ensureProcessDetailsForToolFocus(messageElement, item.processDetailId || ''); + let timeline = detailsContainer && detailsContainer.querySelector('.progress-timeline'); + if (!timeline) return; + let target = null; + if (item.processDetailId) { + target = timeline.querySelector('[data-process-detail-id="' + cssEscapeValue(item.processDetailId) + '"]'); + } + if (!target && item.toolCallId) { + target = timeline.querySelector('[data-tool-call-id="' + cssEscapeValue(item.toolCallId) + '"]'); + } + if (!target) { + const toolItems = timeline.querySelectorAll('.timeline-item-tool_call'); + target = toolItems[index] || null; + } + if (!target && item.processDetailId && messageElement.dataset && messageElement.dataset.backendMessageId && typeof window.loadProcessDetailsPaginated === 'function') { + await window.loadProcessDetailsPaginated(messageElement.id, messageElement.dataset.backendMessageId, { + autoLoadAll: false, + anchorId: item.processDetailId + }); + detailsContainer = document.getElementById('process-details-' + messageElement.id); + timeline = detailsContainer && detailsContainer.querySelector('.progress-timeline'); + target = timeline ? timeline.querySelector('[data-process-detail-id="' + cssEscapeValue(item.processDetailId) + '"]') : null; + } + if (!target) return; + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + target.classList.remove('timeline-item-focus-pulse'); + void target.offsetWidth; + target.classList.add('timeline-item-focus-pulse'); + setTimeout(() => { + target.classList.remove('timeline-item-focus-pulse'); + }, 2200); +} + function toggleMcpToolList(assistantMessageId) { const messageEl = document.getElementById(assistantMessageId); if (!messageEl) return; const chrome = ensureMcpCallSectionChrome(messageEl, assistantMessageId); if (!chrome) return; const { toolList } = chrome; + if ( + !getPendingMcpExecutionCount(messageEl) && + !getPendingToolExecutionSummaryCount(messageEl) && + !toolList.querySelector('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]') + ) { + if (typeof toggleProcessDetails === 'function') { + toggleProcessDetails(null, assistantMessageId); + } + return; + } const willExpand = !toolList.classList.contains('expanded'); if (willExpand) { ensureMcpCallButtons(messageEl); @@ -3007,6 +3233,9 @@ window.syncMcpToolsToggleButton = syncMcpToolsToggleButton; window.isProcessDetailsUserExpanded = isProcessDetailsUserExpanded; window.syncProcessDetailButtonLabels = syncProcessDetailButtonLabels; window.ensureMcpCallSectionChrome = ensureMcpCallSectionChrome; +window.setMcpExecutionSummaryCount = setMcpExecutionSummaryCount; +window.setPendingMcpExecutionIds = setPendingMcpExecutionIds; +window.setPendingToolExecutionSummaries = setPendingToolExecutionSummaries; /** 将 MCP 工具按钮挂到独立工具列表,并批量解析工具名 */ function appendMcpCallButtons(messageElement, executionIds) { @@ -3033,28 +3262,85 @@ function appendMcpCallButtons(messageElement, executionIds) { syncMcpToolsToggleButton(messageElement); } +function appendMcpCallSummaryButtons(messageElement, summaries) { + if (!messageElement || !Array.isArray(summaries) || summaries.length === 0) { + return; + } + const chrome = ensureMcpCallSectionChrome(messageElement, messageElement.id); + if (!chrome) return; + const toolList = chrome.toolList; + summaries.forEach((raw, index) => { + const item = normalizeToolExecutionSummaryForButton(raw); + const key = item.executionId || item.toolCallId || `${item.toolName || 'tool'}-${index + 1}`; + const selector = '.mcp-detail-btn[data-tool-summary="' + CSS.escape(String(key)) + '"]'; + if (toolList.querySelector(selector) || (item.executionId && toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(item.executionId)) + '"]'))) { + return; + } + const btn = document.createElement('button'); + btn.className = 'mcp-detail-btn'; + btn.dataset.toolSummary = key; + if (item.executionId) { + btn.dataset.execId = item.executionId; + } + if (item.processDetailId || item.toolCallId) { + btn.onclick = async () => { + await focusToolExecutionInProcessDetails(messageElement, item, index); + }; + } else if (item.executionId) { + btn.onclick = () => showMCPDetail(item.executionId); + } else { + btn.onclick = async () => { + await focusToolExecutionInProcessDetails(messageElement, item, index); + }; + } + renderToolExecutionButtonContent(btn, item.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'), String(index + 1), item.status); + toolList.appendChild(btn); + }); + syncMcpToolsToggleButton(messageElement); +} + /** 历史会话懒加载:用户展开工具列表时再渲染工具按钮 */ function ensureMcpCallButtons(messageElement) { - if (!messageElement || !messageElement.dataset || !messageElement.dataset.pendingMcpExecutionIds) { + if (!messageElement || !messageElement.dataset) { return; } - let executionIds; - try { - executionIds = JSON.parse(messageElement.dataset.pendingMcpExecutionIds); - } catch (e) { + let renderedSummaryExecutions = false; + if (messageElement.dataset.pendingToolExecutionSummaries) { + let summaries; + try { + summaries = JSON.parse(messageElement.dataset.pendingToolExecutionSummaries); + } catch (e) { + delete messageElement.dataset.pendingToolExecutionSummaries; + summaries = []; + } + if (Array.isArray(summaries) && summaries.length > 0) { + appendMcpCallSummaryButtons(messageElement, summaries); + renderedSummaryExecutions = true; + } + delete messageElement.dataset.pendingToolExecutionSummaries; + } + if (renderedSummaryExecutions) { delete messageElement.dataset.pendingMcpExecutionIds; return; } - if (!Array.isArray(executionIds) || executionIds.length === 0) { + if (messageElement.dataset.pendingMcpExecutionIds) { + let executionIds; + try { + executionIds = JSON.parse(messageElement.dataset.pendingMcpExecutionIds); + } catch (e) { + delete messageElement.dataset.pendingMcpExecutionIds; + executionIds = []; + } + if (Array.isArray(executionIds) && executionIds.length > 0) { + appendMcpCallButtons(messageElement, executionIds); + } delete messageElement.dataset.pendingMcpExecutionIds; - return; } - appendMcpCallButtons(messageElement, executionIds); - delete messageElement.dataset.pendingMcpExecutionIds; } window.ensureMcpCallButtons = ensureMcpCallButtons; window.appendMcpCallButtons = appendMcpCallButtons; +window.appendMcpCallSummaryButtons = appendMcpCallSummaryButtons; function normalizeToolExecutionSummary(raw) { if (typeof raw === 'string') { diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 7fa66102..6a3d7345 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -1376,27 +1376,71 @@ function getProcessDetailsLoadMoreLabel(hasMore) { } function updateProcessDetailsLoadMoreButton(assistantMessageId, backendMessageId, hasMore) { + updateProcessDetailsPaginationButtons(assistantMessageId, backendMessageId, { + hasPrev: false, + hasNext: hasMore + }); +} + +function updateProcessDetailsPaginationButtons(assistantMessageId, backendMessageId, pageState) { const detailsContainer = document.getElementById('process-details-' + assistantMessageId); if (!detailsContainer) return; + const state = pageState || {}; - let btn = detailsContainer.querySelector('.process-details-load-more-btn'); - if (!hasMore) { - if (btn) btn.remove(); + let prevBtn = detailsContainer.querySelector('.process-details-load-prev-btn'); + if (!state.hasPrev) { + if (prevBtn) prevBtn.remove(); + } else { + if (!prevBtn) { + prevBtn = document.createElement('button'); + prevBtn.type = 'button'; + prevBtn.className = 'mcp-detail-btn process-details-load-prev-btn'; + const content = detailsContainer.querySelector('.process-details-content'); + if (content) { + detailsContainer.insertBefore(prevBtn, content); + } else { + detailsContainer.prepend(prevBtn); + } + } + const loadMoreText = typeof window.t === 'function' ? window.t('common.loadMore') : '加载更多'; + let prevPageText = typeof window.t === 'function' ? window.t('chat.previousPage') : '上一页'; + if (!prevPageText || prevPageText === 'chat.previousPage') prevPageText = '上一页'; + prevBtn.textContent = loadMoreText + ' · ' + prevPageText; + prevBtn.disabled = false; + prevBtn.onclick = async () => { + if (detailsContainer.dataset.loadingPrev === '1') return; + detailsContainer.dataset.loadingPrev = '1'; + prevBtn.disabled = true; + prevBtn.textContent = typeof window.t === 'function' ? window.t('common.loading') : '加载中…'; + try { + await loadProcessDetailsPaginated(assistantMessageId, backendMessageId, { + prepend: true, + autoLoadAll: false + }); + } finally { + detailsContainer.dataset.loadingPrev = '0'; + } + }; + } + + let nextBtn = detailsContainer.querySelector('.process-details-load-more-btn'); + if (!state.hasNext) { + if (nextBtn) nextBtn.remove(); return; } - if (!btn) { - btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'mcp-detail-btn process-details-load-more-btn'; - detailsContainer.appendChild(btn); + if (!nextBtn) { + nextBtn = document.createElement('button'); + nextBtn.type = 'button'; + nextBtn.className = 'mcp-detail-btn process-details-load-more-btn'; + detailsContainer.appendChild(nextBtn); } - btn.textContent = getProcessDetailsLoadMoreLabel(true); - btn.disabled = false; - btn.onclick = async () => { + nextBtn.textContent = getProcessDetailsLoadMoreLabel(true); + nextBtn.disabled = false; + nextBtn.onclick = async () => { if (detailsContainer.dataset.loadingMore === '1') return; detailsContainer.dataset.loadingMore = '1'; - btn.disabled = true; - btn.textContent = typeof window.t === 'function' ? window.t('common.loading') : '加载中…'; + nextBtn.disabled = true; + nextBtn.textContent = typeof window.t === 'function' ? window.t('common.loading') : '加载中…'; try { await loadProcessDetailsPaginated(assistantMessageId, backendMessageId, { append: true, @@ -1420,14 +1464,28 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId, const autoLoadAll = opts.autoLoadAll !== false; const detailsContainer = document.getElementById('process-details-' + assistantMessageId); const PAGE = PROCESS_DETAILS_PAGE_SIZE; - let offset = opts.append && detailsContainer && detailsContainer.dataset.nextOffset + const existingNextOffset = detailsContainer && detailsContainer.dataset.nextOffset ? parseInt(detailsContainer.dataset.nextOffset, 10) || 0 : 0; + const prepend = !!opts.prepend; + let offset = prepend && detailsContainer && detailsContainer.dataset.prevOffset + ? parseInt(detailsContainer.dataset.prevOffset, 10) || 0 + : opts.append && detailsContainer && detailsContainer.dataset.nextOffset + ? parseInt(detailsContainer.dataset.nextOffset, 10) || 0 + : 0; + const anchorId = opts.anchorId != null ? String(opts.anchorId).trim() : ''; let isFirst = !opts.append; while (true) { + const params = new URLSearchParams(); + params.set('limit', String(PAGE)); + if (anchorId && !opts.append && !prepend) { + params.set('anchorId', anchorId); + } else { + params.set('offset', String(offset)); + } const res = await apiFetch( '/api/messages/' + encodeURIComponent(String(backendMessageId)) + - '/process-details?limit=' + PAGE + '&offset=' + offset + '/process-details?' + params.toString() ); const j = await res.json().catch(() => ({})); if (!res.ok) { @@ -1436,16 +1494,27 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId, const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : []; const hasMore = !!(j && j.hasMore); renderProcessDetails(assistantMessageId, details, { - append: !isFirst, + append: !isFirst || opts.append, + prepend: prepend, markLoaded: autoLoadAll ? !hasMore : true }); - offset += details.length; + const responseOffset = j && typeof j.offset === 'number' ? j.offset : offset; + const total = j && typeof j.total === 'number' ? j.total : responseOffset + details.length; + const nextOffset = prepend && existingNextOffset > 0 + ? existingNextOffset + : responseOffset + details.length; + const prevOffset = Math.max(0, responseOffset - PAGE); + offset = nextOffset; if (detailsContainer) { detailsContainer.dataset.lazyNotLoaded = '0'; detailsContainer.dataset.loaded = hasMore ? 'partial' : '1'; - detailsContainer.dataset.nextOffset = String(offset); + detailsContainer.dataset.prevOffset = String(prevOffset); + detailsContainer.dataset.nextOffset = String(nextOffset); } - updateProcessDetailsLoadMoreButton(assistantMessageId, backendMessageId, hasMore && !autoLoadAll); + updateProcessDetailsPaginationButtons(assistantMessageId, backendMessageId, { + hasPrev: !autoLoadAll && responseOffset > 0, + hasNext: !autoLoadAll && nextOffset < total + }); if (!hasMore || details.length === 0 || !autoLoadAll) { break; } diff --git a/web/static/js/skills.js b/web/static/js/skills.js index ace155d9..ef35b615 100644 --- a/web/static/js/skills.js +++ b/web/static/js/skills.js @@ -167,18 +167,24 @@ function renderSkillsList() { const fc = typeof skill.file_count === 'number' && skill.file_count > 0 ? _t('skills.cardFiles', { count: skill.file_count }) : ''; - const meta = [ver, fc, sc].filter(Boolean).join(' · '); + const metaItems = [ver, fc, sc].filter(Boolean); + const initial = (skill.name || sid || 'S').trim().charAt(0).toUpperCase(); return `
-
-

${escapeHtml(skill.name || sid)}

- ${meta ? `
${escapeHtml(meta)}
` : ''} -
${escapeHtml(skill.description || _t('skills.noDescription'))}
-
-
- - - + +
+
+
+

${escapeHtml(skill.name || sid)}

+ ${metaItems.length ? `
${metaItems.map(item => `${escapeHtml(item)}`).join('')}
` : ''} +
+
${escapeHtml(skill.description || _t('skills.noDescription'))}
+
+
+ + + +
`; @@ -347,6 +353,27 @@ function updateSkillsManagementStats() { if (totalEl) { totalEl.textContent = skillsPagination.total; } + + const currentCountEl = document.getElementById('skills-current-count'); + if (currentCountEl) { + currentCountEl.textContent = skillsList.length; + } + + const fileCountEl = document.getElementById('skills-file-count'); + if (fileCountEl) { + const fileCount = skillsList.reduce((sum, skill) => { + return sum + (typeof skill.file_count === 'number' ? skill.file_count : 0); + }, 0); + fileCountEl.textContent = fileCount; + } + + const scriptCountEl = document.getElementById('skills-script-count'); + if (scriptCountEl) { + const scriptCount = skillsList.reduce((sum, skill) => { + return sum + (typeof skill.script_count === 'number' ? skill.script_count : 0); + }, 0); + scriptCountEl.textContent = scriptCount; + } } // 搜索skills @@ -920,23 +947,23 @@ function renderSkillsMonitor() { : '0.0'; statsEl.innerHTML = ` -
+
${_t('skills.totalSkillsCount')}
${skillsStats.total}
-
+
${_t('skills.totalCallsCount')}
${skillsStats.totalCalls}
-
+
${_t('skills.successfulCalls')}
-
${skillsStats.totalSuccess}
+
${skillsStats.totalSuccess}
-
+
${_t('skills.failedCalls')}
-
${skillsStats.totalFailed}
+
${skillsStats.totalFailed}
-
+
${_t('skills.successRate')}
${successRate}%
@@ -966,15 +993,15 @@ function renderSkillsMonitor() { }); monitorListEl.innerHTML = ` - +
- - - - - - + + + + + + @@ -984,15 +1011,21 @@ function renderSkillsMonitor() { const failedCalls = stat.failed_calls || 0; const successRate = totalCalls > 0 ? ((successCalls / totalCalls) * 100).toFixed(1) : '0.0'; const lastCallTime = stat.last_call_time && stat.last_call_time !== '-' ? stat.last_call_time : '-'; + const rateValue = Math.max(0, Math.min(100, Number(successRate) || 0)); return ` - - - - - - + + + + + + `; }).join('')} diff --git a/web/templates/index.html b/web/templates/index.html index 06960717..4c6b5522 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -2677,7 +2677,7 @@ -
+
${_t('skills.skillName')}${_t('skills.totalCalls')}${_t('skills.success')}${_t('skills.failure')}${_t('skills.successRate')}${_t('skills.lastCallTime')}${_t('skills.skillName')}${_t('skills.totalCalls')}${_t('skills.success')}${_t('skills.failure')}${_t('skills.successRate')}${_t('skills.lastCallTime')}
${escapeHtml(stat.skill_name || '')}${totalCalls}${successCalls}${failedCalls}${successRate}%${escapeHtml(lastCallTime)}${escapeHtml(stat.skill_name || '')}${totalCalls}${successCalls}${failedCalls} +
+ ${successRate}% + +
+
${escapeHtml(lastCallTime)}