mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 02:22:26 +08:00
✨ 增加會話管理功能
This commit is contained in:
parent
5f4e128f6f
commit
a257744bd1
850
src/mcp_feedback_enhanced/web/static/css/session-management.css
Normal file
850
src/mcp_feedback_enhanced/web/static/css/session-management.css
Normal file
@ -0,0 +1,850 @@
|
||||
/**
|
||||
* 會話管理和連線監控專用樣式
|
||||
* =============================
|
||||
*
|
||||
* 為 WebSocket 連線狀態顯示器和會話管理功能提供樣式支援
|
||||
*/
|
||||
|
||||
/* ===== CSS 變數擴展 ===== */
|
||||
:root {
|
||||
/* 連線狀態色彩 */
|
||||
--status-connected: #4caf50;
|
||||
--status-connecting: #ff9800;
|
||||
--status-disconnected: #f44336;
|
||||
--status-error: #e91e63;
|
||||
--status-reconnecting: #9c27b0;
|
||||
|
||||
/* 會話狀態色彩 */
|
||||
--session-active: #2196f3;
|
||||
--session-waiting: #9c27b0;
|
||||
--session-completed: #4caf50;
|
||||
--session-error: #f44336;
|
||||
--session-timeout: #ff5722;
|
||||
|
||||
/* 面板色彩 - 與主要內容區域統一 */
|
||||
--panel-bg: var(--bg-secondary);
|
||||
--panel-border: var(--border-color);
|
||||
--panel-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
--panel-header-bg: var(--bg-tertiary);
|
||||
|
||||
/* 動畫時間和緩動函數 */
|
||||
--transition-fast: 0.2s;
|
||||
--transition-normal: 0.4s;
|
||||
--transition-slow: 0.6s;
|
||||
--easing-smooth: cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
--easing-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
/* ===== 頂部連線監控狀態列 ===== */
|
||||
.connection-monitor-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--panel-header-bg);
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
padding: 12px 20px;
|
||||
font-size: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 應用資訊區域 */
|
||||
.app-info-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-title h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.countdown-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border: 1px solid var(--warning-color);
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.countdown-timer {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.8;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.connection-status-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex: 2;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 詳細狀態資訊 */
|
||||
.detailed-status-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.websocket-metrics,
|
||||
.session-metrics {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.metric span {
|
||||
color: var(--accent-color);
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.connection-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid transparent;
|
||||
transition: all var(--transition-normal) ease;
|
||||
}
|
||||
|
||||
.connection-indicator.connected {
|
||||
background: rgba(76, 175, 80, 0.15);
|
||||
border-color: var(--status-connected);
|
||||
color: var(--status-connected);
|
||||
}
|
||||
|
||||
.connection-indicator.connecting {
|
||||
background: rgba(255, 152, 0, 0.15);
|
||||
border-color: var(--status-connecting);
|
||||
color: var(--status-connecting);
|
||||
}
|
||||
|
||||
.connection-indicator.disconnected {
|
||||
background: rgba(244, 67, 54, 0.15);
|
||||
border-color: var(--status-disconnected);
|
||||
color: var(--status-disconnected);
|
||||
}
|
||||
|
||||
.connection-indicator.reconnecting {
|
||||
background: rgba(156, 39, 176, 0.15);
|
||||
border-color: var(--status-reconnecting);
|
||||
color: var(--status-reconnecting);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-icon.pulse::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.3;
|
||||
animation: pulse-ring 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% { transform: scale(1); opacity: 0.3; }
|
||||
50% { transform: scale(1.5); opacity: 0.1; }
|
||||
100% { transform: scale(2); opacity: 0; }
|
||||
}
|
||||
|
||||
.connection-quality {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.latency-indicator {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.signal-strength {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.signal-bar {
|
||||
width: 3px;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 1px;
|
||||
transition: background var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.signal-bar:nth-child(2) { height: 8px; }
|
||||
.signal-bar:nth-child(3) { height: 10px; }
|
||||
|
||||
.signal-bar.active {
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.connection-details {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* ===== 會話管理面板 ===== */
|
||||
.session-management-panel {
|
||||
width: 320px;
|
||||
background: var(--panel-bg);
|
||||
border-right: 1px solid var(--panel-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: width var(--transition-slow) var(--easing-smooth),
|
||||
opacity var(--transition-normal) var(--easing-smooth),
|
||||
border-right var(--transition-normal) var(--easing-smooth);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.session-management-panel.collapsed {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
border-right: none;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: var(--panel-header-bg);
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.panel-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ===== 邊緣收合按鈕 ===== */
|
||||
.panel-edge-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -12px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.edge-toggle-btn {
|
||||
width: 24px;
|
||||
height: 48px;
|
||||
background: var(--panel-header-bg);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-left: none;
|
||||
border-radius: 0 8px 8px 0;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition-normal) var(--easing-smooth);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.edge-toggle-btn:hover {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 2px 0 12px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.edge-toggle-btn .toggle-icon {
|
||||
font-size: 12px;
|
||||
transition: transform var(--transition-normal) var(--easing-bounce);
|
||||
}
|
||||
|
||||
.edge-toggle-btn:hover .toggle-icon {
|
||||
transform: translateX(2px) scale(1.1);
|
||||
}
|
||||
|
||||
/* ===== 收合狀態下的展開按鈕 ===== */
|
||||
.collapsed-panel-toggle {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
.collapsed-toggle-btn {
|
||||
width: 40px;
|
||||
height: 80px;
|
||||
background: var(--panel-header-bg);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-left: none;
|
||||
border-radius: 0 12px 12px 0;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
transition: all var(--transition-slow) var(--easing-smooth);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.2);
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
animation: slideInFromLeft var(--transition-slow) var(--easing-smooth);
|
||||
}
|
||||
|
||||
@keyframes slideInFromLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.collapsed-toggle-btn:hover {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
transform: translateX(6px) scale(1.05);
|
||||
width: 46px;
|
||||
box-shadow: 4px 0 16px rgba(0, 122, 204, 0.4);
|
||||
}
|
||||
|
||||
.collapsed-toggle-btn .toggle-icon {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.collapsed-toggle-btn .toggle-text {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* ===== 會話卡片 ===== */
|
||||
.session-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
transition: all var(--transition-fast) ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-card:hover {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 122, 204, 0.2);
|
||||
}
|
||||
|
||||
.session-card.active {
|
||||
border-color: var(--session-active);
|
||||
background: rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
.session-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.session-id {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-badge.waiting {
|
||||
background: rgba(156, 39, 176, 0.2);
|
||||
color: var(--session-waiting);
|
||||
border: 1px solid var(--session-waiting);
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: rgba(33, 150, 243, 0.2);
|
||||
color: var(--session-active);
|
||||
border: 1px solid var(--session-active);
|
||||
}
|
||||
|
||||
.status-badge.completed {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: var(--session-completed);
|
||||
border: 1px solid var(--session-completed);
|
||||
}
|
||||
|
||||
.status-badge.error {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: var(--session-error);
|
||||
border: 1px solid var(--session-error);
|
||||
}
|
||||
|
||||
.session-info {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.session-time,
|
||||
.session-project,
|
||||
.session-duration {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.session-summary {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
max-height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.session-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* ===== 會話統計 ===== */
|
||||
.session-stats-section {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--accent-color);
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ===== 響應式設計 ===== */
|
||||
@media (max-width: 1200px) {
|
||||
.session-management-panel {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
box-shadow: var(--panel-shadow);
|
||||
}
|
||||
|
||||
.session-management-panel.collapsed {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.panel-edge-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsed-panel-toggle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.session-management-panel {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 60vh;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.session-management-panel.collapsed {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.session-management-panel:not(.collapsed) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.connection-monitor-bar {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.app-info-section {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.app-title h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.connection-status-group {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detailed-status-info {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 載入狀態 ===== */
|
||||
.loading-skeleton {
|
||||
background: linear-gradient(90deg,
|
||||
var(--bg-tertiary) 25%,
|
||||
rgba(255, 255, 255, 0.1) 50%,
|
||||
var(--bg-tertiary) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading-shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading-shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* ===== 無障礙支援 ===== */
|
||||
.session-card:focus {
|
||||
outline: 2px solid var(--accent-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-icon:focus,
|
||||
.btn-small:focus {
|
||||
outline: 2px solid var(--accent-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ===== 會話詳情彈窗 ===== */
|
||||
.session-details-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
animation: modalSlideIn var(--transition-normal) var(--easing-smooth);
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
min-width: 80px;
|
||||
margin-right: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-value.session-id {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.detail-value.project-path {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-value.summary {
|
||||
background: var(--bg-secondary);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--accent-color);
|
||||
line-height: 1.4;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 8px 16px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* 減少動畫偏好 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
@ -814,9 +814,30 @@ body {
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.main-content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px 0 0 0;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 當會話面板收合時,主內容區域擴展 */
|
||||
.main-content.panel-collapsed .main-content-area {
|
||||
margin-left: 0;
|
||||
border-radius: 8px 0 0 0;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 分頁樣式 */
|
||||
|
@ -3,8 +3,9 @@
|
||||
* =================================
|
||||
*
|
||||
* 模組化重構版本,整合所有功能模組
|
||||
* 依賴模組載入順序:utils -> tab-manager -> websocket-manager -> image-handler ->
|
||||
* settings-manager -> ui-manager -> auto-refresh-manager -> app
|
||||
* 依賴模組載入順序:utils -> tab-manager -> websocket-manager -> connection-monitor ->
|
||||
* session-manager -> image-handler -> settings-manager -> ui-manager ->
|
||||
* auto-refresh-manager -> app
|
||||
*/
|
||||
|
||||
(function() {
|
||||
@ -25,6 +26,8 @@
|
||||
// 模組管理器
|
||||
this.tabManager = null;
|
||||
this.webSocketManager = null;
|
||||
this.connectionMonitor = null;
|
||||
this.sessionManager = null;
|
||||
this.imageHandler = null;
|
||||
this.settingsManager = null;
|
||||
this.uiManager = null;
|
||||
@ -127,9 +130,30 @@
|
||||
// 4. 初始化標籤頁管理器
|
||||
self.tabManager = new window.MCPFeedback.TabManager();
|
||||
|
||||
// 5. 初始化 WebSocket 管理器
|
||||
// 5. 初始化連線監控器
|
||||
self.connectionMonitor = new window.MCPFeedback.ConnectionMonitor({
|
||||
onStatusChange: function(status, message) {
|
||||
console.log('🔍 連線狀態變更:', status, message);
|
||||
},
|
||||
onQualityChange: function(quality, latency) {
|
||||
console.log('🔍 連線品質變更:', quality, latency + 'ms');
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 初始化會話管理器
|
||||
self.sessionManager = new window.MCPFeedback.SessionManager({
|
||||
onSessionChange: function(sessionData) {
|
||||
console.log('📋 會話變更:', sessionData);
|
||||
},
|
||||
onSessionSelect: function(sessionId) {
|
||||
console.log('📋 會話選擇:', sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// 7. 初始化 WebSocket 管理器
|
||||
self.webSocketManager = new window.MCPFeedback.WebSocketManager({
|
||||
tabManager: self.tabManager,
|
||||
connectionMonitor: self.connectionMonitor,
|
||||
onOpen: function() {
|
||||
self.handleWebSocketOpen();
|
||||
},
|
||||
@ -141,10 +165,14 @@
|
||||
},
|
||||
onConnectionStatusChange: function(status, text) {
|
||||
self.uiManager.updateConnectionStatus(status, text);
|
||||
// 同時更新連線監控器
|
||||
if (self.connectionMonitor) {
|
||||
self.connectionMonitor.updateConnectionStatus(status, text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 6. 初始化圖片處理器
|
||||
// 8. 初始化圖片處理器
|
||||
self.imageHandler = new window.MCPFeedback.ImageHandler({
|
||||
imageSizeLimit: settings.imageSizeLimit,
|
||||
enableBase64Detail: settings.enableBase64Detail,
|
||||
@ -154,7 +182,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 7. 初始化自動刷新管理器
|
||||
// 9. 初始化自動刷新管理器
|
||||
self.autoRefreshManager = new window.MCPFeedback.AutoRefreshManager({
|
||||
autoRefreshEnabled: settings.autoRefreshEnabled,
|
||||
autoRefreshInterval: settings.autoRefreshInterval,
|
||||
@ -453,9 +481,69 @@
|
||||
const newSessionId = data.session_info.session_id;
|
||||
console.log('📋 會話 ID 更新: ' + this.currentSessionId + ' -> ' + newSessionId);
|
||||
|
||||
// 保存舊會話到歷史記錄(在更新當前會話之前)
|
||||
if (this.currentSessionId && this.sessionManager && this.currentSessionId !== newSessionId) {
|
||||
console.log('📋 嘗試獲取當前會話數據...');
|
||||
// 從 SessionManager 獲取當前會話的完整數據
|
||||
const currentSessionData = this.sessionManager.getCurrentSessionData();
|
||||
console.log('📋 從 currentSession 獲取數據:', this.currentSessionId);
|
||||
|
||||
if (currentSessionData) {
|
||||
// 計算實際持續時間
|
||||
const now = Date.now() / 1000;
|
||||
let duration = 300; // 預設 5 分鐘
|
||||
|
||||
if (currentSessionData.created_at) {
|
||||
let createdAt = currentSessionData.created_at;
|
||||
// 處理時間戳格式
|
||||
if (createdAt > 1e12) {
|
||||
createdAt = createdAt / 1000;
|
||||
}
|
||||
duration = Math.max(1, Math.round(now - createdAt));
|
||||
}
|
||||
|
||||
const oldSessionData = {
|
||||
session_id: this.currentSessionId,
|
||||
status: 'completed',
|
||||
created_at: currentSessionData.created_at || (now - duration),
|
||||
completed_at: now,
|
||||
duration: duration,
|
||||
project_directory: currentSessionData.project_directory,
|
||||
summary: currentSessionData.summary
|
||||
};
|
||||
|
||||
console.log('📋 準備將舊會話加入歷史記錄:', oldSessionData);
|
||||
|
||||
// 先更新當前會話 ID,再調用 addSessionToHistory
|
||||
this.currentSessionId = newSessionId;
|
||||
|
||||
// 更新會話管理器的當前會話(這樣 addSessionToHistory 檢查時就不會認為是當前活躍會話)
|
||||
if (this.sessionManager) {
|
||||
this.sessionManager.updateCurrentSession(data.session_info);
|
||||
}
|
||||
|
||||
// 現在可以安全地將舊會話加入歷史記錄
|
||||
this.sessionManager.addSessionToHistory(oldSessionData);
|
||||
} else {
|
||||
console.log('⚠️ 無法獲取當前會話數據,跳過歷史記錄保存');
|
||||
// 仍然需要更新當前會話 ID
|
||||
this.currentSessionId = newSessionId;
|
||||
// 更新會話管理器
|
||||
if (this.sessionManager) {
|
||||
this.sessionManager.updateCurrentSession(data.session_info);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 沒有舊會話或會話 ID 相同,直接更新
|
||||
this.currentSessionId = newSessionId;
|
||||
// 更新會話管理器
|
||||
if (this.sessionManager) {
|
||||
this.sessionManager.updateCurrentSession(data.session_info);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置回饋狀態為等待新回饋
|
||||
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, newSessionId);
|
||||
this.currentSessionId = newSessionId;
|
||||
|
||||
// 更新自動刷新管理器的會話 ID
|
||||
if (this.autoRefreshManager) {
|
||||
@ -484,6 +572,11 @@
|
||||
FeedbackApp.prototype.handleStatusUpdate = function(statusInfo) {
|
||||
console.log('處理狀態更新:', statusInfo);
|
||||
|
||||
// 更新 SessionManager 的狀態資訊
|
||||
if (this.sessionManager && this.sessionManager.updateStatusInfo) {
|
||||
this.sessionManager.updateStatusInfo(statusInfo);
|
||||
}
|
||||
|
||||
// 更新頁面標題顯示會話信息
|
||||
if (statusInfo.project_directory) {
|
||||
const projectName = statusInfo.project_directory.split(/[/\\]/).pop();
|
||||
@ -820,6 +913,14 @@
|
||||
this.webSocketManager.close();
|
||||
}
|
||||
|
||||
if (this.connectionMonitor) {
|
||||
this.connectionMonitor.cleanup();
|
||||
}
|
||||
|
||||
if (this.sessionManager) {
|
||||
this.sessionManager.cleanup();
|
||||
}
|
||||
|
||||
if (this.imageHandler) {
|
||||
this.imageHandler.cleanup();
|
||||
}
|
||||
|
@ -0,0 +1,358 @@
|
||||
/**
|
||||
* MCP Feedback Enhanced - 連線監控模組
|
||||
* ===================================
|
||||
*
|
||||
* 處理 WebSocket 連線狀態監控、品質檢測和診斷功能
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 確保命名空間和依賴存在
|
||||
window.MCPFeedback = window.MCPFeedback || {};
|
||||
const Utils = window.MCPFeedback.Utils;
|
||||
|
||||
/**
|
||||
* 連線監控器建構函數
|
||||
*/
|
||||
function ConnectionMonitor(options) {
|
||||
options = options || {};
|
||||
|
||||
// 監控狀態
|
||||
this.isMonitoring = false;
|
||||
this.connectionStartTime = null;
|
||||
this.lastPingTime = null;
|
||||
this.latencyHistory = [];
|
||||
this.maxLatencyHistory = 20;
|
||||
this.reconnectCount = 0;
|
||||
this.messageCount = 0;
|
||||
|
||||
// 連線品質指標
|
||||
this.currentLatency = 0;
|
||||
this.averageLatency = 0;
|
||||
this.connectionQuality = 'unknown'; // excellent, good, fair, poor, unknown
|
||||
|
||||
// UI 元素
|
||||
this.statusIcon = null;
|
||||
this.statusText = null;
|
||||
this.latencyDisplay = null;
|
||||
this.connectionTimeDisplay = null;
|
||||
this.reconnectCountDisplay = null;
|
||||
this.messageCountDisplay = null;
|
||||
this.signalBars = null;
|
||||
|
||||
// 回調函數
|
||||
this.onStatusChange = options.onStatusChange || null;
|
||||
this.onQualityChange = options.onQualityChange || null;
|
||||
|
||||
this.initializeUI();
|
||||
|
||||
console.log('🔍 ConnectionMonitor 初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 UI 元素
|
||||
*/
|
||||
ConnectionMonitor.prototype.initializeUI = function() {
|
||||
// 獲取 UI 元素引用
|
||||
this.statusIcon = Utils.safeQuerySelector('.status-icon');
|
||||
this.statusText = Utils.safeQuerySelector('.status-text');
|
||||
this.latencyDisplay = Utils.safeQuerySelector('.latency-indicator');
|
||||
this.connectionTimeDisplay = Utils.safeQuerySelector('.connection-time');
|
||||
this.reconnectCountDisplay = Utils.safeQuerySelector('.reconnect-count');
|
||||
this.messageCountDisplay = Utils.safeQuerySelector('#messageCount');
|
||||
this.latencyDisplayFooter = Utils.safeQuerySelector('#latencyDisplay');
|
||||
this.signalBars = document.querySelectorAll('.signal-bar');
|
||||
|
||||
// 初始化顯示
|
||||
this.updateDisplay();
|
||||
};
|
||||
|
||||
/**
|
||||
* 開始監控
|
||||
*/
|
||||
ConnectionMonitor.prototype.startMonitoring = function() {
|
||||
if (this.isMonitoring) return;
|
||||
|
||||
this.isMonitoring = true;
|
||||
this.connectionStartTime = Date.now();
|
||||
this.reconnectCount = 0;
|
||||
this.messageCount = 0;
|
||||
this.latencyHistory = [];
|
||||
|
||||
console.log('🔍 開始連線監控');
|
||||
this.updateDisplay();
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止監控
|
||||
*/
|
||||
ConnectionMonitor.prototype.stopMonitoring = function() {
|
||||
this.isMonitoring = false;
|
||||
this.connectionStartTime = null;
|
||||
this.lastPingTime = null;
|
||||
|
||||
console.log('🔍 停止連線監控');
|
||||
this.updateDisplay();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新連線狀態
|
||||
*/
|
||||
ConnectionMonitor.prototype.updateConnectionStatus = function(status, message) {
|
||||
console.log('🔍 連線狀態更新:', status, message);
|
||||
|
||||
// 更新狀態顯示
|
||||
if (this.statusText) {
|
||||
this.statusText.textContent = message || status;
|
||||
}
|
||||
|
||||
// 更新狀態圖示
|
||||
if (this.statusIcon) {
|
||||
this.statusIcon.className = 'status-icon';
|
||||
|
||||
switch (status) {
|
||||
case 'connecting':
|
||||
case 'reconnecting':
|
||||
this.statusIcon.classList.add('pulse');
|
||||
break;
|
||||
case 'connected':
|
||||
this.statusIcon.classList.remove('pulse');
|
||||
break;
|
||||
default:
|
||||
this.statusIcon.classList.remove('pulse');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新連線指示器樣式
|
||||
const indicator = Utils.safeQuerySelector('.connection-indicator');
|
||||
if (indicator) {
|
||||
indicator.className = 'connection-indicator ' + status;
|
||||
}
|
||||
|
||||
// 處理特殊狀態
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
if (!this.isMonitoring) {
|
||||
this.startMonitoring();
|
||||
}
|
||||
break;
|
||||
case 'disconnected':
|
||||
case 'error':
|
||||
this.stopMonitoring();
|
||||
break;
|
||||
case 'reconnecting':
|
||||
this.reconnectCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
|
||||
// 調用回調
|
||||
if (this.onStatusChange) {
|
||||
this.onStatusChange(status, message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 記錄 ping 時間
|
||||
*/
|
||||
ConnectionMonitor.prototype.recordPing = function() {
|
||||
this.lastPingTime = Date.now();
|
||||
};
|
||||
|
||||
/**
|
||||
* 記錄 pong 時間並計算延遲
|
||||
*/
|
||||
ConnectionMonitor.prototype.recordPong = function() {
|
||||
if (!this.lastPingTime) return;
|
||||
|
||||
const now = Date.now();
|
||||
const latency = now - this.lastPingTime;
|
||||
|
||||
this.currentLatency = latency;
|
||||
this.latencyHistory.push(latency);
|
||||
|
||||
// 保持歷史記錄在限制範圍內
|
||||
if (this.latencyHistory.length > this.maxLatencyHistory) {
|
||||
this.latencyHistory.shift();
|
||||
}
|
||||
|
||||
// 計算平均延遲
|
||||
this.averageLatency = this.latencyHistory.reduce((sum, lat) => sum + lat, 0) / this.latencyHistory.length;
|
||||
|
||||
// 更新連線品質
|
||||
this.updateConnectionQuality();
|
||||
|
||||
console.log('🔍 延遲測量:', latency + 'ms', '平均:', Math.round(this.averageLatency) + 'ms');
|
||||
|
||||
this.updateDisplay();
|
||||
};
|
||||
|
||||
/**
|
||||
* 記錄訊息
|
||||
*/
|
||||
ConnectionMonitor.prototype.recordMessage = function() {
|
||||
this.messageCount++;
|
||||
this.updateDisplay();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新連線品質
|
||||
*/
|
||||
ConnectionMonitor.prototype.updateConnectionQuality = function() {
|
||||
const avgLatency = this.averageLatency;
|
||||
let quality;
|
||||
|
||||
if (avgLatency < 50) {
|
||||
quality = 'excellent';
|
||||
} else if (avgLatency < 100) {
|
||||
quality = 'good';
|
||||
} else if (avgLatency < 200) {
|
||||
quality = 'fair';
|
||||
} else {
|
||||
quality = 'poor';
|
||||
}
|
||||
|
||||
if (quality !== this.connectionQuality) {
|
||||
this.connectionQuality = quality;
|
||||
this.updateSignalStrength();
|
||||
|
||||
if (this.onQualityChange) {
|
||||
this.onQualityChange(quality, avgLatency);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新信號強度顯示
|
||||
*/
|
||||
ConnectionMonitor.prototype.updateSignalStrength = function() {
|
||||
if (!this.signalBars || this.signalBars.length === 0) return;
|
||||
|
||||
let activeBars = 0;
|
||||
|
||||
switch (this.connectionQuality) {
|
||||
case 'excellent':
|
||||
activeBars = 3;
|
||||
break;
|
||||
case 'good':
|
||||
activeBars = 2;
|
||||
break;
|
||||
case 'fair':
|
||||
activeBars = 1;
|
||||
break;
|
||||
case 'poor':
|
||||
default:
|
||||
activeBars = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
this.signalBars.forEach(function(bar, index) {
|
||||
if (index < activeBars) {
|
||||
bar.classList.add('active');
|
||||
} else {
|
||||
bar.classList.remove('active');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新顯示
|
||||
*/
|
||||
ConnectionMonitor.prototype.updateDisplay = function() {
|
||||
// 更新延遲顯示
|
||||
if (this.latencyDisplay) {
|
||||
if (this.currentLatency > 0) {
|
||||
this.latencyDisplay.textContent = '延遲: ' + this.currentLatency + 'ms';
|
||||
} else {
|
||||
this.latencyDisplay.textContent = '延遲: --ms';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.latencyDisplayFooter) {
|
||||
if (this.currentLatency > 0) {
|
||||
this.latencyDisplayFooter.textContent = this.currentLatency + 'ms';
|
||||
} else {
|
||||
this.latencyDisplayFooter.textContent = '--ms';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新連線時間
|
||||
if (this.connectionTimeDisplay && this.connectionStartTime) {
|
||||
const duration = Math.floor((Date.now() - this.connectionStartTime) / 1000);
|
||||
const minutes = Math.floor(duration / 60);
|
||||
const seconds = duration % 60;
|
||||
this.connectionTimeDisplay.textContent = '連線時間: ' +
|
||||
String(minutes).padStart(2, '0') + ':' +
|
||||
String(seconds).padStart(2, '0');
|
||||
}
|
||||
|
||||
// 更新重連次數
|
||||
if (this.reconnectCountDisplay) {
|
||||
this.reconnectCountDisplay.textContent = '重連: ' + this.reconnectCount + ' 次';
|
||||
}
|
||||
|
||||
// 更新訊息計數
|
||||
if (this.messageCountDisplay) {
|
||||
this.messageCountDisplay.textContent = this.messageCount;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取連線統計資訊
|
||||
*/
|
||||
ConnectionMonitor.prototype.getConnectionStats = function() {
|
||||
return {
|
||||
isMonitoring: this.isMonitoring,
|
||||
connectionTime: this.connectionStartTime ? Date.now() - this.connectionStartTime : 0,
|
||||
currentLatency: this.currentLatency,
|
||||
averageLatency: Math.round(this.averageLatency),
|
||||
connectionQuality: this.connectionQuality,
|
||||
reconnectCount: this.reconnectCount,
|
||||
messageCount: this.messageCount,
|
||||
latencyHistory: this.latencyHistory.slice() // 複製陣列
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置統計
|
||||
*/
|
||||
ConnectionMonitor.prototype.resetStats = function() {
|
||||
this.reconnectCount = 0;
|
||||
this.messageCount = 0;
|
||||
this.latencyHistory = [];
|
||||
this.currentLatency = 0;
|
||||
this.averageLatency = 0;
|
||||
this.connectionQuality = 'unknown';
|
||||
|
||||
this.updateDisplay();
|
||||
this.updateSignalStrength();
|
||||
|
||||
console.log('🔍 連線統計已重置');
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理資源
|
||||
*/
|
||||
ConnectionMonitor.prototype.cleanup = function() {
|
||||
this.stopMonitoring();
|
||||
|
||||
// 清理 UI 引用
|
||||
this.statusIcon = null;
|
||||
this.statusText = null;
|
||||
this.latencyDisplay = null;
|
||||
this.connectionTimeDisplay = null;
|
||||
this.reconnectCountDisplay = null;
|
||||
this.messageCountDisplay = null;
|
||||
this.signalBars = null;
|
||||
|
||||
console.log('🔍 ConnectionMonitor 清理完成');
|
||||
};
|
||||
|
||||
// 將 ConnectionMonitor 加入命名空間
|
||||
window.MCPFeedback.ConnectionMonitor = ConnectionMonitor;
|
||||
|
||||
console.log('✅ ConnectionMonitor 模組載入完成');
|
||||
|
||||
})();
|
@ -0,0 +1,538 @@
|
||||
/**
|
||||
* MCP Feedback Enhanced - 會話管理模組(重構版)
|
||||
* =============================================
|
||||
*
|
||||
* 整合會話數據管理、UI 渲染和面板控制功能
|
||||
* 使用模組化架構提升可維護性
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 確保命名空間和依賴存在
|
||||
window.MCPFeedback = window.MCPFeedback || {};
|
||||
|
||||
// 獲取 DOMUtils 的安全方法
|
||||
function getDOMUtils() {
|
||||
return window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.DOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* 會話管理器建構函數(重構版)
|
||||
*/
|
||||
function SessionManager(options) {
|
||||
options = options || {};
|
||||
|
||||
// 子模組實例
|
||||
this.dataManager = null;
|
||||
this.uiRenderer = null;
|
||||
this.detailsModal = null;
|
||||
|
||||
// UI 狀態
|
||||
this.isPanelVisible = true;
|
||||
this.isLoading = false;
|
||||
|
||||
// UI 元素
|
||||
this.panel = null;
|
||||
this.edgeToggleBtn = null;
|
||||
this.collapsedToggleBtn = null;
|
||||
this.mainContent = null;
|
||||
|
||||
// 回調函數
|
||||
this.onSessionChange = options.onSessionChange || null;
|
||||
this.onSessionSelect = options.onSessionSelect || null;
|
||||
|
||||
this.initializeModules(options);
|
||||
this.initializeUI();
|
||||
|
||||
console.log('📋 SessionManager (重構版) 初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化子模組
|
||||
*/
|
||||
SessionManager.prototype.initializeModules = function(options) {
|
||||
const self = this;
|
||||
|
||||
// 初始化數據管理器
|
||||
this.dataManager = new window.MCPFeedback.Session.DataManager({
|
||||
onSessionChange: function(sessionData) {
|
||||
self.handleSessionChange(sessionData);
|
||||
},
|
||||
onHistoryChange: function(history) {
|
||||
self.handleHistoryChange(history);
|
||||
},
|
||||
onStatsChange: function(stats) {
|
||||
self.handleStatsChange(stats);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化 UI 渲染器
|
||||
this.uiRenderer = new window.MCPFeedback.Session.UIRenderer({
|
||||
showFullSessionId: options.showFullSessionId || false,
|
||||
enableAnimations: options.enableAnimations !== false
|
||||
});
|
||||
|
||||
// 初始化詳情彈窗
|
||||
this.detailsModal = new window.MCPFeedback.Session.DetailsModal({
|
||||
enableEscapeClose: options.enableEscapeClose !== false,
|
||||
enableBackdropClose: options.enableBackdropClose !== false,
|
||||
showFullSessionId: options.showFullSessionId || false
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化 UI 元素
|
||||
*/
|
||||
SessionManager.prototype.initializeUI = function() {
|
||||
const DOMUtils = getDOMUtils();
|
||||
|
||||
if (!DOMUtils) {
|
||||
console.warn('📋 DOMUtils 尚未載入,使用原生 DOM 方法');
|
||||
// 使用原生 DOM 方法作為後備
|
||||
this.panel = document.querySelector('.session-management-panel');
|
||||
this.edgeToggleBtn = document.querySelector('#edgeToggleBtn');
|
||||
this.collapsedToggleBtn = document.querySelector('#collapsedToggleBtn');
|
||||
this.mainContent = document.querySelector('.main-content');
|
||||
} else {
|
||||
// 使用 DOMUtils
|
||||
this.panel = DOMUtils.safeQuerySelector('.session-management-panel');
|
||||
this.edgeToggleBtn = DOMUtils.safeQuerySelector('#edgeToggleBtn');
|
||||
this.collapsedToggleBtn = DOMUtils.safeQuerySelector('#collapsedToggleBtn');
|
||||
this.mainContent = DOMUtils.safeQuerySelector('.main-content');
|
||||
}
|
||||
|
||||
// 設置事件監聽器
|
||||
this.setupEventListeners();
|
||||
|
||||
// 初始化顯示
|
||||
this.updateDisplay();
|
||||
};
|
||||
|
||||
/**
|
||||
* 處理會話變更
|
||||
*/
|
||||
SessionManager.prototype.handleSessionChange = function(sessionData) {
|
||||
console.log('📋 處理會話變更:', sessionData);
|
||||
|
||||
// 更新 UI 渲染
|
||||
this.uiRenderer.renderCurrentSession(sessionData);
|
||||
|
||||
// 調用外部回調
|
||||
if (this.onSessionChange) {
|
||||
this.onSessionChange(sessionData);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 處理歷史記錄變更
|
||||
*/
|
||||
SessionManager.prototype.handleHistoryChange = function(history) {
|
||||
console.log('📋 處理歷史記錄變更:', history.length, '個會話');
|
||||
|
||||
// 更新 UI 渲染
|
||||
this.uiRenderer.renderSessionHistory(history);
|
||||
};
|
||||
|
||||
/**
|
||||
* 處理統計資訊變更
|
||||
*/
|
||||
SessionManager.prototype.handleStatsChange = function(stats) {
|
||||
console.log('📋 處理統計資訊變更:', stats);
|
||||
|
||||
// 更新 UI 渲染
|
||||
this.uiRenderer.renderStats(stats);
|
||||
};
|
||||
|
||||
/**
|
||||
* 設置事件監聽器
|
||||
*/
|
||||
SessionManager.prototype.setupEventListeners = function() {
|
||||
const self = this;
|
||||
const DOMUtils = getDOMUtils();
|
||||
|
||||
// 邊緣收合按鈕
|
||||
if (this.edgeToggleBtn) {
|
||||
this.edgeToggleBtn.addEventListener('click', function() {
|
||||
self.togglePanel();
|
||||
});
|
||||
}
|
||||
|
||||
// 收合狀態下的展開按鈕
|
||||
if (this.collapsedToggleBtn) {
|
||||
this.collapsedToggleBtn.addEventListener('click', function() {
|
||||
self.togglePanel();
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新按鈕
|
||||
const refreshButton = DOMUtils ?
|
||||
DOMUtils.safeQuerySelector('#refreshSessions') :
|
||||
document.querySelector('#refreshSessions');
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener('click', function() {
|
||||
self.refreshSessionData();
|
||||
});
|
||||
}
|
||||
|
||||
// 詳細資訊按鈕
|
||||
const detailsButton = DOMUtils ?
|
||||
DOMUtils.safeQuerySelector('#viewSessionDetails') :
|
||||
document.querySelector('#viewSessionDetails');
|
||||
if (detailsButton) {
|
||||
detailsButton.addEventListener('click', function() {
|
||||
self.showSessionDetails();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新當前會話(委託給數據管理器)
|
||||
*/
|
||||
SessionManager.prototype.updateCurrentSession = function(sessionData) {
|
||||
return this.dataManager.updateCurrentSession(sessionData);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新狀態資訊(委託給數據管理器)
|
||||
*/
|
||||
SessionManager.prototype.updateStatusInfo = function(statusInfo) {
|
||||
return this.dataManager.updateStatusInfo(statusInfo);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 切換面板顯示
|
||||
*/
|
||||
SessionManager.prototype.togglePanel = function() {
|
||||
if (!this.panel) return;
|
||||
|
||||
const DOMUtils = getDOMUtils();
|
||||
this.isPanelVisible = !this.isPanelVisible;
|
||||
|
||||
if (this.isPanelVisible) {
|
||||
// 展開面板
|
||||
this.panel.classList.remove('collapsed');
|
||||
if (this.mainContent) {
|
||||
this.mainContent.classList.remove('panel-collapsed');
|
||||
}
|
||||
|
||||
// 隱藏收合狀態下的展開按鈕
|
||||
const collapsedToggle = DOMUtils ?
|
||||
DOMUtils.safeQuerySelector('#collapsedPanelToggle') :
|
||||
document.querySelector('#collapsedPanelToggle');
|
||||
if (collapsedToggle) {
|
||||
collapsedToggle.style.display = 'none';
|
||||
}
|
||||
|
||||
// 更新邊緣按鈕圖示和提示
|
||||
this.updateToggleButton('◀', '收合面板');
|
||||
} else {
|
||||
// 收合面板
|
||||
this.panel.classList.add('collapsed');
|
||||
if (this.mainContent) {
|
||||
this.mainContent.classList.add('panel-collapsed');
|
||||
}
|
||||
|
||||
// 顯示收合狀態下的展開按鈕
|
||||
const collapsedToggle = DOMUtils ?
|
||||
DOMUtils.safeQuerySelector('#collapsedPanelToggle') :
|
||||
document.querySelector('#collapsedPanelToggle');
|
||||
if (collapsedToggle) {
|
||||
collapsedToggle.style.display = 'block';
|
||||
}
|
||||
|
||||
// 更新邊緣按鈕圖示和提示
|
||||
this.updateToggleButton('▶', '展開面板');
|
||||
}
|
||||
|
||||
console.log('📋 會話面板', this.isPanelVisible ? '顯示' : '隱藏');
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新切換按鈕
|
||||
*/
|
||||
SessionManager.prototype.updateToggleButton = function(iconText, title) {
|
||||
if (this.edgeToggleBtn) {
|
||||
const icon = this.edgeToggleBtn.querySelector('.toggle-icon');
|
||||
if (icon) {
|
||||
icon.textContent = iconText;
|
||||
}
|
||||
this.edgeToggleBtn.setAttribute('title', title);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新會話數據
|
||||
*/
|
||||
SessionManager.prototype.refreshSessionData = function() {
|
||||
if (this.isLoading) return;
|
||||
|
||||
console.log('📋 刷新會話數據');
|
||||
this.isLoading = true;
|
||||
|
||||
const self = this;
|
||||
// 這裡可以發送 WebSocket 請求獲取最新數據
|
||||
setTimeout(function() {
|
||||
self.isLoading = false;
|
||||
console.log('📋 會話數據刷新完成');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* 顯示當前會話詳情
|
||||
*/
|
||||
SessionManager.prototype.showSessionDetails = function() {
|
||||
const currentSession = this.dataManager.getCurrentSession();
|
||||
|
||||
if (!currentSession) {
|
||||
this.showMessage('目前沒有活躍的會話數據', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.detailsModal.showSessionDetails(currentSession);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 查看會話詳情(通過會話ID)
|
||||
*/
|
||||
SessionManager.prototype.viewSessionDetails = function(sessionId) {
|
||||
console.log('📋 查看會話詳情:', sessionId);
|
||||
|
||||
const sessionData = this.dataManager.findSessionById(sessionId);
|
||||
|
||||
if (sessionData) {
|
||||
this.detailsModal.showSessionDetails(sessionData);
|
||||
} else {
|
||||
this.showMessage('找不到會話資料', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 獲取當前會話(便利方法)
|
||||
*/
|
||||
SessionManager.prototype.getCurrentSession = function() {
|
||||
return this.dataManager.getCurrentSession();
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取會話歷史(便利方法)
|
||||
*/
|
||||
SessionManager.prototype.getSessionHistory = function() {
|
||||
return this.dataManager.getSessionHistory();
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取統計資訊(便利方法)
|
||||
*/
|
||||
SessionManager.prototype.getStats = function() {
|
||||
return this.dataManager.getStats();
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取當前會話數據(相容性方法)
|
||||
*/
|
||||
SessionManager.prototype.getCurrentSessionData = function() {
|
||||
console.log('📋 嘗試獲取當前會話數據...');
|
||||
|
||||
const currentSession = this.dataManager.getCurrentSession();
|
||||
|
||||
if (currentSession && currentSession.session_id) {
|
||||
console.log('📋 從 dataManager 獲取數據:', currentSession.session_id);
|
||||
return currentSession;
|
||||
}
|
||||
|
||||
// 嘗試從 app 的 WebSocketManager 獲取
|
||||
if (window.feedbackApp && window.feedbackApp.webSocketManager) {
|
||||
const wsManager = window.feedbackApp.webSocketManager;
|
||||
if (wsManager.sessionId) {
|
||||
console.log('📋 從 WebSocketManager 獲取數據:', wsManager.sessionId);
|
||||
return {
|
||||
session_id: wsManager.sessionId,
|
||||
status: this.getCurrentSessionStatus(),
|
||||
created_at: this.getSessionCreatedTime(),
|
||||
project_directory: this.getProjectDirectory(),
|
||||
summary: this.getAISummary()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 嘗試從 app 的 currentSessionId 獲取
|
||||
if (window.feedbackApp && window.feedbackApp.currentSessionId) {
|
||||
console.log('📋 從 app.currentSessionId 獲取數據:', window.feedbackApp.currentSessionId);
|
||||
return {
|
||||
session_id: window.feedbackApp.currentSessionId,
|
||||
status: this.getCurrentSessionStatus(),
|
||||
created_at: this.getSessionCreatedTime(),
|
||||
project_directory: this.getProjectDirectory(),
|
||||
summary: this.getAISummary()
|
||||
};
|
||||
}
|
||||
|
||||
console.log('📋 無法獲取會話數據');
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取會話建立時間
|
||||
*/
|
||||
SessionManager.prototype.getSessionCreatedTime = function() {
|
||||
// 嘗試從 WebSocketManager 的連線開始時間獲取
|
||||
if (window.feedbackApp && window.feedbackApp.webSocketManager) {
|
||||
const wsManager = window.feedbackApp.webSocketManager;
|
||||
if (wsManager.connectionStartTime) {
|
||||
return wsManager.connectionStartTime / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// 嘗試從最後收到的狀態更新中獲取
|
||||
if (this.dataManager && this.dataManager.lastStatusUpdate && this.dataManager.lastStatusUpdate.created_at) {
|
||||
return this.dataManager.lastStatusUpdate.created_at;
|
||||
}
|
||||
|
||||
// 如果都沒有,返回 null
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取當前會話狀態
|
||||
*/
|
||||
SessionManager.prototype.getCurrentSessionStatus = function() {
|
||||
// 嘗試從 UIManager 獲取當前狀態
|
||||
if (window.feedbackApp && window.feedbackApp.uiManager) {
|
||||
const currentState = window.feedbackApp.uiManager.getFeedbackState();
|
||||
if (currentState) {
|
||||
// 將內部狀態轉換為會話狀態
|
||||
const stateMap = {
|
||||
'waiting_for_feedback': 'waiting',
|
||||
'processing': 'active',
|
||||
'feedback_submitted': 'feedback_submitted'
|
||||
};
|
||||
return stateMap[currentState] || currentState;
|
||||
}
|
||||
}
|
||||
|
||||
// 嘗試從最後收到的狀態更新中獲取
|
||||
if (this.dataManager && this.dataManager.lastStatusUpdate && this.dataManager.lastStatusUpdate.status) {
|
||||
return this.dataManager.lastStatusUpdate.status;
|
||||
}
|
||||
|
||||
// 預設狀態
|
||||
return 'waiting';
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取專案目錄
|
||||
*/
|
||||
SessionManager.prototype.getProjectDirectory = function() {
|
||||
const projectElement = document.querySelector('.session-project');
|
||||
if (projectElement) {
|
||||
return projectElement.textContent.replace('專案: ', '');
|
||||
}
|
||||
|
||||
// 從頂部狀態列獲取
|
||||
const topProjectInfo = document.querySelector('.project-info');
|
||||
if (topProjectInfo) {
|
||||
return topProjectInfo.textContent.replace('專案目錄: ', '');
|
||||
}
|
||||
|
||||
return '未知';
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取 AI 摘要
|
||||
*/
|
||||
SessionManager.prototype.getAISummary = function() {
|
||||
const summaryElement = document.querySelector('.session-summary');
|
||||
if (summaryElement && summaryElement.textContent !== 'AI 摘要: 載入中...') {
|
||||
return summaryElement.textContent.replace('AI 摘要: ', '');
|
||||
}
|
||||
|
||||
// 嘗試從主要內容區域獲取
|
||||
const mainSummary = document.querySelector('#combinedSummaryContent');
|
||||
if (mainSummary && mainSummary.textContent.trim()) {
|
||||
return mainSummary.textContent.trim();
|
||||
}
|
||||
|
||||
return '暫無摘要';
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新顯示
|
||||
*/
|
||||
SessionManager.prototype.updateDisplay = function() {
|
||||
const currentSession = this.dataManager.getCurrentSession();
|
||||
const history = this.dataManager.getSessionHistory();
|
||||
const stats = this.dataManager.getStats();
|
||||
|
||||
this.uiRenderer.renderCurrentSession(currentSession);
|
||||
this.uiRenderer.renderSessionHistory(history);
|
||||
this.uiRenderer.renderStats(stats);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理資源
|
||||
*/
|
||||
SessionManager.prototype.cleanup = function() {
|
||||
// 清理子模組
|
||||
if (this.dataManager) {
|
||||
this.dataManager.cleanup();
|
||||
this.dataManager = null;
|
||||
}
|
||||
|
||||
if (this.uiRenderer) {
|
||||
this.uiRenderer.cleanup();
|
||||
this.uiRenderer = null;
|
||||
}
|
||||
|
||||
if (this.detailsModal) {
|
||||
this.detailsModal.cleanup();
|
||||
this.detailsModal = null;
|
||||
}
|
||||
|
||||
// 清理 UI 引用
|
||||
this.panel = null;
|
||||
this.edgeToggleBtn = null;
|
||||
this.collapsedToggleBtn = null;
|
||||
this.mainContent = null;
|
||||
|
||||
console.log('📋 SessionManager (重構版) 清理完成');
|
||||
};
|
||||
|
||||
// 將 SessionManager 加入命名空間
|
||||
window.MCPFeedback.SessionManager = SessionManager;
|
||||
|
||||
// 全域方法供 HTML 調用
|
||||
window.MCPFeedback.SessionManager.viewSessionDetails = function(sessionId) {
|
||||
console.log('📋 全域查看會話詳情:', sessionId);
|
||||
|
||||
// 找到當前的 SessionManager 實例
|
||||
if (window.MCPFeedback && window.MCPFeedback.app && window.MCPFeedback.app.sessionManager) {
|
||||
const sessionManager = window.MCPFeedback.app.sessionManager;
|
||||
sessionManager.viewSessionDetails(sessionId);
|
||||
} else {
|
||||
// 如果找不到實例,顯示錯誤訊息
|
||||
console.warn('找不到 SessionManager 實例');
|
||||
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
|
||||
window.MCPFeedback.Utils.showMessage('會話管理器未初始化', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('✅ SessionManager (重構版) 模組載入完成');
|
||||
|
||||
})();
|
@ -0,0 +1,379 @@
|
||||
/**
|
||||
* MCP Feedback Enhanced - 會話數據管理模組
|
||||
* ========================================
|
||||
*
|
||||
* 負責會話數據的存儲、更新和狀態管理
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 確保命名空間存在
|
||||
window.MCPFeedback = window.MCPFeedback || {};
|
||||
window.MCPFeedback.Session = window.MCPFeedback.Session || {};
|
||||
|
||||
const TimeUtils = window.MCPFeedback.Utils.Time;
|
||||
const StatusUtils = window.MCPFeedback.Utils.Status;
|
||||
|
||||
/**
|
||||
* 會話數據管理器
|
||||
*/
|
||||
function SessionDataManager(options) {
|
||||
options = options || {};
|
||||
|
||||
// 會話數據
|
||||
this.currentSession = null;
|
||||
this.sessionHistory = [];
|
||||
this.lastStatusUpdate = null;
|
||||
|
||||
// 統計數據
|
||||
this.sessionStats = {
|
||||
todayCount: 0,
|
||||
averageDuration: 0,
|
||||
totalSessions: 0
|
||||
};
|
||||
|
||||
// 回調函數
|
||||
this.onSessionChange = options.onSessionChange || null;
|
||||
this.onHistoryChange = options.onHistoryChange || null;
|
||||
this.onStatsChange = options.onStatsChange || null;
|
||||
|
||||
console.log('📊 SessionDataManager 初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新當前會話
|
||||
*/
|
||||
SessionDataManager.prototype.updateCurrentSession = function(sessionData) {
|
||||
console.log('📊 更新當前會話:', sessionData);
|
||||
|
||||
if (this.currentSession && this.currentSession.session_id === sessionData.session_id) {
|
||||
// 合併數據,保留重要資訊
|
||||
this.currentSession = this.mergeSessionData(this.currentSession, sessionData);
|
||||
} else {
|
||||
// 新會話或不同會話 ID - 需要處理舊會話
|
||||
if (this.currentSession && this.currentSession.session_id) {
|
||||
console.log('📊 檢測到會話 ID 變更,處理舊會話:', this.currentSession.session_id, '->', sessionData.session_id);
|
||||
|
||||
// 將舊會話標記為完成並加入歷史記錄
|
||||
const oldSession = Object.assign({}, this.currentSession);
|
||||
oldSession.status = 'completed';
|
||||
oldSession.completed_at = TimeUtils.getCurrentTimestamp();
|
||||
|
||||
// 計算持續時間
|
||||
if (oldSession.created_at && !oldSession.duration) {
|
||||
oldSession.duration = oldSession.completed_at - oldSession.created_at;
|
||||
}
|
||||
|
||||
console.log('📊 將舊會話加入歷史記錄:', oldSession);
|
||||
this.addSessionToHistory(oldSession);
|
||||
}
|
||||
|
||||
// 設置新會話
|
||||
this.currentSession = this.normalizeSessionData(sessionData);
|
||||
}
|
||||
|
||||
// 觸發回調
|
||||
if (this.onSessionChange) {
|
||||
this.onSessionChange(this.currentSession);
|
||||
}
|
||||
|
||||
return this.currentSession;
|
||||
};
|
||||
|
||||
/**
|
||||
* 合併會話數據
|
||||
*/
|
||||
SessionDataManager.prototype.mergeSessionData = function(existingData, newData) {
|
||||
const merged = Object.assign({}, existingData, newData);
|
||||
|
||||
// 確保重要欄位不會被覆蓋為空值
|
||||
if (!merged.created_at && existingData.created_at) {
|
||||
merged.created_at = existingData.created_at;
|
||||
}
|
||||
|
||||
if (!merged.status && existingData.status) {
|
||||
merged.status = existingData.status;
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
/**
|
||||
* 標準化會話數據
|
||||
*/
|
||||
SessionDataManager.prototype.normalizeSessionData = function(sessionData) {
|
||||
const normalized = Object.assign({}, sessionData);
|
||||
|
||||
// 補充缺失的時間戳
|
||||
if (!normalized.created_at) {
|
||||
if (this.lastStatusUpdate && this.lastStatusUpdate.created_at) {
|
||||
normalized.created_at = this.lastStatusUpdate.created_at;
|
||||
} else {
|
||||
normalized.created_at = TimeUtils.getCurrentTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
// 補充缺失的狀態
|
||||
if (!normalized.status) {
|
||||
normalized.status = 'waiting';
|
||||
}
|
||||
|
||||
// 標準化時間戳
|
||||
if (normalized.created_at) {
|
||||
normalized.created_at = TimeUtils.normalizeTimestamp(normalized.created_at);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新狀態資訊
|
||||
*/
|
||||
SessionDataManager.prototype.updateStatusInfo = function(statusInfo) {
|
||||
console.log('📊 更新狀態資訊:', statusInfo);
|
||||
|
||||
this.lastStatusUpdate = statusInfo;
|
||||
|
||||
if (statusInfo.session_id || statusInfo.created_at) {
|
||||
const sessionData = {
|
||||
session_id: statusInfo.session_id || (this.currentSession && this.currentSession.session_id),
|
||||
status: statusInfo.status,
|
||||
created_at: statusInfo.created_at,
|
||||
project_directory: statusInfo.project_directory || this.getProjectDirectory(),
|
||||
summary: statusInfo.summary || this.getAISummary()
|
||||
};
|
||||
|
||||
// 檢查會話是否完成
|
||||
if (StatusUtils.isCompletedStatus(statusInfo.status)) {
|
||||
this.handleSessionCompleted(sessionData);
|
||||
} else {
|
||||
this.updateCurrentSession(sessionData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 處理會話完成
|
||||
*/
|
||||
SessionDataManager.prototype.handleSessionCompleted = function(sessionData) {
|
||||
console.log('📊 處理會話完成:', sessionData);
|
||||
|
||||
// 確保會話有完成時間
|
||||
if (!sessionData.completed_at) {
|
||||
sessionData.completed_at = TimeUtils.getCurrentTimestamp();
|
||||
}
|
||||
|
||||
// 計算持續時間
|
||||
if (sessionData.created_at && !sessionData.duration) {
|
||||
sessionData.duration = sessionData.completed_at - sessionData.created_at;
|
||||
}
|
||||
|
||||
// 將完成的會話加入歷史記錄
|
||||
this.addSessionToHistory(sessionData);
|
||||
|
||||
// 如果是當前會話完成,保持引用但標記為完成
|
||||
if (this.currentSession && this.currentSession.session_id === sessionData.session_id) {
|
||||
this.currentSession = Object.assign(this.currentSession, sessionData);
|
||||
if (this.onSessionChange) {
|
||||
this.onSessionChange(this.currentSession);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增會話到歷史記錄
|
||||
*/
|
||||
SessionDataManager.prototype.addSessionToHistory = function(sessionData) {
|
||||
console.log('📊 新增會話到歷史記錄:', sessionData);
|
||||
|
||||
// 只有已完成的會話才加入歷史記錄
|
||||
if (!StatusUtils.isCompletedStatus(sessionData.status)) {
|
||||
console.log('📊 跳過未完成的會話:', sessionData.session_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 避免重複新增
|
||||
const existingIndex = this.sessionHistory.findIndex(s => s.session_id === sessionData.session_id);
|
||||
if (existingIndex !== -1) {
|
||||
this.sessionHistory[existingIndex] = sessionData;
|
||||
} else {
|
||||
this.sessionHistory.unshift(sessionData);
|
||||
}
|
||||
|
||||
// 限制歷史記錄數量
|
||||
if (this.sessionHistory.length > 10) {
|
||||
this.sessionHistory = this.sessionHistory.slice(0, 10);
|
||||
}
|
||||
|
||||
this.updateStats();
|
||||
|
||||
// 觸發回調
|
||||
if (this.onHistoryChange) {
|
||||
this.onHistoryChange(this.sessionHistory);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取當前會話
|
||||
*/
|
||||
SessionDataManager.prototype.getCurrentSession = function() {
|
||||
return this.currentSession;
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取會話歷史
|
||||
*/
|
||||
SessionDataManager.prototype.getSessionHistory = function() {
|
||||
return this.sessionHistory.slice(); // 返回副本
|
||||
};
|
||||
|
||||
/**
|
||||
* 根據 ID 查找會話
|
||||
*/
|
||||
SessionDataManager.prototype.findSessionById = function(sessionId) {
|
||||
// 先檢查當前會話
|
||||
if (this.currentSession && this.currentSession.session_id === sessionId) {
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
// 再檢查歷史記錄
|
||||
return this.sessionHistory.find(s => s.session_id === sessionId) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新統計資訊
|
||||
*/
|
||||
SessionDataManager.prototype.updateStats = function() {
|
||||
// 計算今日會話數
|
||||
const todayStart = TimeUtils.getTodayStartTimestamp();
|
||||
this.sessionStats.todayCount = this.sessionHistory.filter(function(session) {
|
||||
return session.created_at && session.created_at >= todayStart;
|
||||
}).length;
|
||||
|
||||
// 計算平均持續時間
|
||||
const completedSessions = this.sessionHistory.filter(s => s.duration && s.duration > 0);
|
||||
if (completedSessions.length > 0) {
|
||||
const totalDuration = completedSessions.reduce((sum, s) => sum + s.duration, 0);
|
||||
this.sessionStats.averageDuration = Math.round(totalDuration / completedSessions.length);
|
||||
} else {
|
||||
this.sessionStats.averageDuration = 0;
|
||||
}
|
||||
|
||||
this.sessionStats.totalSessions = this.sessionHistory.length;
|
||||
|
||||
// 觸發回調
|
||||
if (this.onStatsChange) {
|
||||
this.onStatsChange(this.sessionStats);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取統計資訊
|
||||
*/
|
||||
SessionDataManager.prototype.getStats = function() {
|
||||
return Object.assign({}, this.sessionStats);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空會話數據
|
||||
*/
|
||||
SessionDataManager.prototype.clearCurrentSession = function() {
|
||||
this.currentSession = null;
|
||||
if (this.onSessionChange) {
|
||||
this.onSessionChange(null);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空歷史記錄
|
||||
*/
|
||||
SessionDataManager.prototype.clearHistory = function() {
|
||||
this.sessionHistory = [];
|
||||
this.updateStats();
|
||||
if (this.onHistoryChange) {
|
||||
this.onHistoryChange(this.sessionHistory);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取專案目錄(輔助方法)
|
||||
*/
|
||||
SessionDataManager.prototype.getProjectDirectory = function() {
|
||||
// 嘗試從多個來源獲取專案目錄
|
||||
const sources = [
|
||||
() => document.querySelector('.session-project')?.textContent?.replace('專案: ', ''),
|
||||
() => document.querySelector('.project-info')?.textContent?.replace('專案目錄: ', ''),
|
||||
() => this.currentSession?.project_directory
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const result = source();
|
||||
if (result && result !== '未知') {
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略錯誤,繼續嘗試下一個來源
|
||||
}
|
||||
}
|
||||
|
||||
return '未知';
|
||||
};
|
||||
|
||||
/**
|
||||
* 獲取 AI 摘要(輔助方法)
|
||||
*/
|
||||
SessionDataManager.prototype.getAISummary = function() {
|
||||
// 嘗試從多個來源獲取 AI 摘要
|
||||
const sources = [
|
||||
() => {
|
||||
const element = document.querySelector('.session-summary');
|
||||
const text = element?.textContent;
|
||||
return text && text !== 'AI 摘要: 載入中...' ? text.replace('AI 摘要: ', '') : null;
|
||||
},
|
||||
() => {
|
||||
const element = document.querySelector('#combinedSummaryContent');
|
||||
return element?.textContent?.trim();
|
||||
},
|
||||
() => this.currentSession?.summary
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const result = source();
|
||||
if (result && result !== '暫無摘要') {
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略錯誤,繼續嘗試下一個來源
|
||||
}
|
||||
}
|
||||
|
||||
return '暫無摘要';
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理資源
|
||||
*/
|
||||
SessionDataManager.prototype.cleanup = function() {
|
||||
this.currentSession = null;
|
||||
this.sessionHistory = [];
|
||||
this.lastStatusUpdate = null;
|
||||
this.sessionStats = {
|
||||
todayCount: 0,
|
||||
averageDuration: 0,
|
||||
totalSessions: 0
|
||||
};
|
||||
|
||||
console.log('📊 SessionDataManager 清理完成');
|
||||
};
|
||||
|
||||
// 將 SessionDataManager 加入命名空間
|
||||
window.MCPFeedback.Session.DataManager = SessionDataManager;
|
||||
|
||||
console.log('✅ SessionDataManager 模組載入完成');
|
||||
|
||||
})();
|
@ -0,0 +1,315 @@
|
||||
/**
|
||||
* MCP Feedback Enhanced - 會話詳情彈窗模組
|
||||
* =======================================
|
||||
*
|
||||
* 負責會話詳情彈窗的創建、顯示和管理
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 確保命名空間存在
|
||||
window.MCPFeedback = window.MCPFeedback || {};
|
||||
window.MCPFeedback.Session = window.MCPFeedback.Session || {};
|
||||
|
||||
const DOMUtils = window.MCPFeedback.Utils.DOM;
|
||||
const TimeUtils = window.MCPFeedback.Utils.Time;
|
||||
const StatusUtils = window.MCPFeedback.Utils.Status;
|
||||
|
||||
/**
|
||||
* 會話詳情彈窗管理器
|
||||
*/
|
||||
function SessionDetailsModal(options) {
|
||||
options = options || {};
|
||||
|
||||
// 彈窗選項
|
||||
this.enableEscapeClose = options.enableEscapeClose !== false;
|
||||
this.enableBackdropClose = options.enableBackdropClose !== false;
|
||||
this.showFullSessionId = options.showFullSessionId || false;
|
||||
|
||||
// 當前彈窗引用
|
||||
this.currentModal = null;
|
||||
this.keydownHandler = null;
|
||||
|
||||
console.log('🔍 SessionDetailsModal 初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 顯示會話詳情
|
||||
*/
|
||||
SessionDetailsModal.prototype.showSessionDetails = function(sessionData) {
|
||||
if (!sessionData) {
|
||||
this.showError('沒有可顯示的會話數據');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 顯示會話詳情:', sessionData.session_id);
|
||||
|
||||
// 關閉現有彈窗
|
||||
this.closeModal();
|
||||
|
||||
// 格式化會話詳情
|
||||
const details = this.formatSessionDetails(sessionData);
|
||||
|
||||
// 創建並顯示彈窗
|
||||
this.createAndShowModal(details);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化會話詳情
|
||||
*/
|
||||
SessionDetailsModal.prototype.formatSessionDetails = function(sessionData) {
|
||||
console.log('🔍 格式化會話詳情:', sessionData);
|
||||
|
||||
// 處理會話 ID
|
||||
const sessionId = this.showFullSessionId ?
|
||||
(sessionData.session_id || '未知') :
|
||||
(sessionData.session_id || '未知').substring(0, 16) + '...';
|
||||
|
||||
// 處理建立時間
|
||||
const createdTime = sessionData.created_at ?
|
||||
TimeUtils.formatTimestamp(sessionData.created_at) :
|
||||
'未知';
|
||||
|
||||
// 處理持續時間
|
||||
let duration = '進行中';
|
||||
if (sessionData.duration && sessionData.duration > 0) {
|
||||
duration = TimeUtils.formatDuration(sessionData.duration);
|
||||
} else if (sessionData.created_at && sessionData.completed_at) {
|
||||
const durationSeconds = sessionData.completed_at - sessionData.created_at;
|
||||
duration = TimeUtils.formatDuration(durationSeconds);
|
||||
} else if (sessionData.created_at) {
|
||||
const elapsed = TimeUtils.calculateElapsedTime(sessionData.created_at);
|
||||
if (elapsed > 0) {
|
||||
duration = TimeUtils.formatDuration(elapsed) + ' (進行中)';
|
||||
}
|
||||
}
|
||||
|
||||
// 處理狀態
|
||||
const status = sessionData.status || 'waiting';
|
||||
const statusText = StatusUtils.getStatusText(status);
|
||||
const statusColor = StatusUtils.getStatusColor(status);
|
||||
|
||||
return {
|
||||
sessionId: sessionId,
|
||||
status: statusText,
|
||||
statusColor: statusColor,
|
||||
createdTime: createdTime,
|
||||
duration: duration,
|
||||
projectDirectory: sessionData.project_directory || '未知',
|
||||
summary: sessionData.summary || '暫無摘要'
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 創建並顯示彈窗
|
||||
*/
|
||||
SessionDetailsModal.prototype.createAndShowModal = function(details) {
|
||||
// 創建彈窗 HTML
|
||||
const modalHtml = this.createModalHTML(details);
|
||||
|
||||
// 插入到頁面中
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
// 獲取彈窗元素
|
||||
this.currentModal = document.getElementById('sessionDetailsModal');
|
||||
|
||||
// 設置事件監聽器
|
||||
this.setupEventListeners();
|
||||
|
||||
// 添加顯示動畫
|
||||
this.showModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* 創建彈窗 HTML
|
||||
*/
|
||||
SessionDetailsModal.prototype.createModalHTML = function(details) {
|
||||
return `
|
||||
<div class="session-details-modal" id="sessionDetailsModal">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>會話詳細資訊</h3>
|
||||
<button class="modal-close" id="closeSessionDetails" aria-label="關閉">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">會話 ID:</span>
|
||||
<span class="detail-value session-id" title="${details.sessionId}">${details.sessionId}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">狀態:</span>
|
||||
<span class="detail-value" style="color: ${details.statusColor};">${details.status}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">建立時間:</span>
|
||||
<span class="detail-value">${details.createdTime}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">持續時間:</span>
|
||||
<span class="detail-value">${details.duration}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">專案目錄:</span>
|
||||
<span class="detail-value project-path" title="${details.projectDirectory}">${details.projectDirectory}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">AI 摘要:</span>
|
||||
<div class="detail-value summary">${this.escapeHtml(details.summary)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" id="closeSessionDetailsBtn">關閉</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 設置事件監聽器
|
||||
*/
|
||||
SessionDetailsModal.prototype.setupEventListeners = function() {
|
||||
if (!this.currentModal) return;
|
||||
|
||||
const self = this;
|
||||
|
||||
// 關閉按鈕
|
||||
const closeBtn = this.currentModal.querySelector('#closeSessionDetails');
|
||||
const closeFooterBtn = this.currentModal.querySelector('#closeSessionDetailsBtn');
|
||||
|
||||
if (closeBtn) {
|
||||
DOMUtils.addEventListener(closeBtn, 'click', function() {
|
||||
self.closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (closeFooterBtn) {
|
||||
DOMUtils.addEventListener(closeFooterBtn, 'click', function() {
|
||||
self.closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// 背景點擊關閉
|
||||
if (this.enableBackdropClose) {
|
||||
const backdrop = this.currentModal.querySelector('.modal-backdrop');
|
||||
if (backdrop) {
|
||||
DOMUtils.addEventListener(backdrop, 'click', function() {
|
||||
self.closeModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ESC 鍵關閉
|
||||
if (this.enableEscapeClose) {
|
||||
this.keydownHandler = function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
self.closeModal();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', this.keydownHandler);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 顯示彈窗動畫
|
||||
*/
|
||||
SessionDetailsModal.prototype.showModal = function() {
|
||||
if (!this.currentModal) return;
|
||||
|
||||
// 添加顯示類觸發動畫
|
||||
requestAnimationFrame(() => {
|
||||
DOMUtils.safeAddClass(this.currentModal, 'show');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 關閉彈窗
|
||||
*/
|
||||
SessionDetailsModal.prototype.closeModal = function() {
|
||||
if (!this.currentModal) return;
|
||||
|
||||
// 移除鍵盤事件監聽器
|
||||
if (this.keydownHandler) {
|
||||
document.removeEventListener('keydown', this.keydownHandler);
|
||||
this.keydownHandler = null;
|
||||
}
|
||||
|
||||
// 添加關閉動畫
|
||||
DOMUtils.safeAddClass(this.currentModal, 'hide');
|
||||
|
||||
// 延遲移除元素
|
||||
setTimeout(() => {
|
||||
if (this.currentModal) {
|
||||
DOMUtils.safeRemoveElement(this.currentModal);
|
||||
this.currentModal = null;
|
||||
}
|
||||
}, 300); // 與 CSS 動畫時間一致
|
||||
};
|
||||
|
||||
/**
|
||||
* 顯示錯誤訊息
|
||||
*/
|
||||
SessionDetailsModal.prototype.showError = function(message) {
|
||||
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
|
||||
window.MCPFeedback.Utils.showMessage(message, 'error');
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* HTML 轉義
|
||||
*/
|
||||
SessionDetailsModal.prototype.escapeHtml = function(text) {
|
||||
if (!text) return '';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
/**
|
||||
* 檢查是否有彈窗開啟
|
||||
*/
|
||||
SessionDetailsModal.prototype.isModalOpen = function() {
|
||||
return this.currentModal !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 強制關閉所有彈窗
|
||||
*/
|
||||
SessionDetailsModal.prototype.forceCloseAll = function() {
|
||||
// 關閉當前彈窗
|
||||
this.closeModal();
|
||||
|
||||
// 清理可能遺留的彈窗元素
|
||||
const existingModals = document.querySelectorAll('.session-details-modal');
|
||||
existingModals.forEach(modal => {
|
||||
DOMUtils.safeRemoveElement(modal);
|
||||
});
|
||||
|
||||
// 清理事件監聽器
|
||||
if (this.keydownHandler) {
|
||||
document.removeEventListener('keydown', this.keydownHandler);
|
||||
this.keydownHandler = null;
|
||||
}
|
||||
|
||||
this.currentModal = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理資源
|
||||
*/
|
||||
SessionDetailsModal.prototype.cleanup = function() {
|
||||
this.forceCloseAll();
|
||||
console.log('🔍 SessionDetailsModal 清理完成');
|
||||
};
|
||||
|
||||
// 將 SessionDetailsModal 加入命名空間
|
||||
window.MCPFeedback.Session.DetailsModal = SessionDetailsModal;
|
||||
|
||||
console.log('✅ SessionDetailsModal 模組載入完成');
|
||||
|
||||
})();
|
@ -0,0 +1,447 @@
|
||||
/**
|
||||
* MCP Feedback Enhanced - 會話 UI 渲染模組
|
||||
* =======================================
|
||||
*
|
||||
* 負責會話相關的 UI 渲染和更新
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 確保命名空間存在
|
||||
window.MCPFeedback = window.MCPFeedback || {};
|
||||
window.MCPFeedback.Session = window.MCPFeedback.Session || {};
|
||||
|
||||
const DOMUtils = window.MCPFeedback.Utils.DOM;
|
||||
const TimeUtils = window.MCPFeedback.Utils.Time;
|
||||
const StatusUtils = window.MCPFeedback.Utils.Status;
|
||||
|
||||
/**
|
||||
* 會話 UI 渲染器
|
||||
*/
|
||||
function SessionUIRenderer(options) {
|
||||
options = options || {};
|
||||
|
||||
// UI 元素引用
|
||||
this.currentSessionCard = null;
|
||||
this.historyList = null;
|
||||
this.statsElements = {};
|
||||
|
||||
// 渲染選項
|
||||
this.showFullSessionId = options.showFullSessionId || false;
|
||||
this.enableAnimations = options.enableAnimations !== false;
|
||||
|
||||
// 活躍時間定時器
|
||||
this.activeTimeTimer = null;
|
||||
this.currentSessionData = null;
|
||||
|
||||
this.initializeElements();
|
||||
this.startActiveTimeTimer();
|
||||
|
||||
console.log('🎨 SessionUIRenderer 初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 UI 元素
|
||||
*/
|
||||
SessionUIRenderer.prototype.initializeElements = function() {
|
||||
this.currentSessionCard = DOMUtils.safeQuerySelector('#currentSessionCard');
|
||||
this.historyList = DOMUtils.safeQuerySelector('#sessionHistoryList');
|
||||
|
||||
// 統計元素
|
||||
this.statsElements = {
|
||||
todayCount: DOMUtils.safeQuerySelector('.stat-today-count'),
|
||||
averageDuration: DOMUtils.safeQuerySelector('.stat-average-duration'),
|
||||
totalSessions: DOMUtils.safeQuerySelector('.stat-total-sessions')
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染當前會話
|
||||
*/
|
||||
SessionUIRenderer.prototype.renderCurrentSession = function(sessionData) {
|
||||
if (!this.currentSessionCard || !sessionData) return;
|
||||
|
||||
console.log('🎨 渲染當前會話:', sessionData);
|
||||
|
||||
// 檢查是否是新會話(會話 ID 變更)
|
||||
const isNewSession = !this.currentSessionData ||
|
||||
this.currentSessionData.session_id !== sessionData.session_id;
|
||||
|
||||
// 更新當前會話數據
|
||||
this.currentSessionData = sessionData;
|
||||
|
||||
// 如果是新會話,重置活躍時間定時器
|
||||
if (isNewSession) {
|
||||
console.log('🎨 檢測到新會話,重置活躍時間定時器');
|
||||
this.resetActiveTimeTimer();
|
||||
}
|
||||
|
||||
// 更新會話 ID
|
||||
this.updateSessionId(sessionData);
|
||||
|
||||
// 更新狀態徽章
|
||||
this.updateStatusBadge(sessionData);
|
||||
|
||||
// 更新時間資訊
|
||||
this.updateTimeInfo(sessionData);
|
||||
|
||||
// 更新專案資訊
|
||||
this.updateProjectInfo(sessionData);
|
||||
|
||||
// 更新摘要
|
||||
this.updateSummary(sessionData);
|
||||
|
||||
// 更新會話狀態列
|
||||
this.updateSessionStatusBar(sessionData);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新會話 ID 顯示
|
||||
*/
|
||||
SessionUIRenderer.prototype.updateSessionId = function(sessionData) {
|
||||
const sessionIdElement = this.currentSessionCard.querySelector('.session-id');
|
||||
if (sessionIdElement && sessionData.session_id) {
|
||||
const displayId = this.showFullSessionId ?
|
||||
sessionData.session_id :
|
||||
sessionData.session_id.substring(0, 8) + '...';
|
||||
DOMUtils.safeSetTextContent(sessionIdElement, '會話 ID: ' + displayId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新狀態徽章
|
||||
*/
|
||||
SessionUIRenderer.prototype.updateStatusBadge = function(sessionData) {
|
||||
const statusBadge = this.currentSessionCard.querySelector('.status-badge');
|
||||
if (statusBadge && sessionData.status) {
|
||||
StatusUtils.updateStatusIndicator(statusBadge, sessionData.status, {
|
||||
updateText: true,
|
||||
updateColor: false, // 使用 CSS 類控制顏色
|
||||
updateClass: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新時間資訊
|
||||
*/
|
||||
SessionUIRenderer.prototype.updateTimeInfo = function(sessionData) {
|
||||
const timeElement = this.currentSessionCard.querySelector('.session-time');
|
||||
if (timeElement && sessionData.created_at) {
|
||||
const timeText = TimeUtils.formatTimestamp(sessionData.created_at, { format: 'time' });
|
||||
DOMUtils.safeSetTextContent(timeElement, '建立時間: ' + timeText);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新專案資訊
|
||||
*/
|
||||
SessionUIRenderer.prototype.updateProjectInfo = function(sessionData) {
|
||||
const projectElement = this.currentSessionCard.querySelector('.session-project');
|
||||
if (projectElement) {
|
||||
const projectDir = sessionData.project_directory || './';
|
||||
DOMUtils.safeSetTextContent(projectElement, '專案: ' + projectDir);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新摘要
|
||||
*/
|
||||
SessionUIRenderer.prototype.updateSummary = function(sessionData) {
|
||||
const summaryElement = this.currentSessionCard.querySelector('.session-summary');
|
||||
if (summaryElement) {
|
||||
const summary = sessionData.summary || '無摘要';
|
||||
DOMUtils.safeSetTextContent(summaryElement, 'AI 摘要: ' + summary);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新會話狀態列
|
||||
*/
|
||||
SessionUIRenderer.prototype.updateSessionStatusBar = function(sessionData) {
|
||||
if (!sessionData) return;
|
||||
|
||||
console.log('🎨 更新會話狀態列:', sessionData);
|
||||
|
||||
// 更新當前會話 ID
|
||||
const currentSessionElement = document.getElementById('currentSessionId');
|
||||
if (currentSessionElement && sessionData.session_id) {
|
||||
const shortId = sessionData.session_id.substring(0, 8) + '...';
|
||||
DOMUtils.safeSetTextContent(currentSessionElement, shortId);
|
||||
}
|
||||
|
||||
// 立即更新活躍時間(定時器會持續更新)
|
||||
this.updateActiveTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染會話歷史列表
|
||||
*/
|
||||
SessionUIRenderer.prototype.renderSessionHistory = function(sessionHistory) {
|
||||
if (!this.historyList) return;
|
||||
|
||||
console.log('🎨 渲染會話歷史:', sessionHistory.length, '個會話');
|
||||
|
||||
// 清空現有內容
|
||||
DOMUtils.clearElement(this.historyList);
|
||||
|
||||
if (sessionHistory.length === 0) {
|
||||
this.renderEmptyHistory();
|
||||
return;
|
||||
}
|
||||
|
||||
// 渲染歷史會話
|
||||
const fragment = document.createDocumentFragment();
|
||||
sessionHistory.forEach((session) => {
|
||||
const card = this.createSessionCard(session, true);
|
||||
fragment.appendChild(card);
|
||||
});
|
||||
|
||||
this.historyList.appendChild(fragment);
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染空歷史狀態
|
||||
*/
|
||||
SessionUIRenderer.prototype.renderEmptyHistory = function() {
|
||||
const emptyElement = DOMUtils.createElement('div', {
|
||||
className: 'no-sessions',
|
||||
textContent: '暫無歷史會話'
|
||||
});
|
||||
this.historyList.appendChild(emptyElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* 創建會話卡片
|
||||
*/
|
||||
SessionUIRenderer.prototype.createSessionCard = function(sessionData, isHistory) {
|
||||
const card = DOMUtils.createElement('div', {
|
||||
className: 'session-card' + (isHistory ? ' history' : ''),
|
||||
attributes: {
|
||||
'data-session-id': sessionData.session_id
|
||||
}
|
||||
});
|
||||
|
||||
// 創建卡片內容
|
||||
const header = this.createSessionHeader(sessionData);
|
||||
const info = this.createSessionInfo(sessionData, isHistory);
|
||||
const actions = this.createSessionActions(sessionData, isHistory);
|
||||
|
||||
card.appendChild(header);
|
||||
card.appendChild(info);
|
||||
card.appendChild(actions);
|
||||
|
||||
return card;
|
||||
};
|
||||
|
||||
/**
|
||||
* 創建會話卡片標題
|
||||
*/
|
||||
SessionUIRenderer.prototype.createSessionHeader = function(sessionData) {
|
||||
const header = DOMUtils.createElement('div', { className: 'session-header' });
|
||||
|
||||
// 會話 ID
|
||||
const sessionId = DOMUtils.createElement('div', {
|
||||
className: 'session-id',
|
||||
textContent: '會話 ID: ' + (sessionData.session_id || '').substring(0, 8) + '...'
|
||||
});
|
||||
|
||||
// 狀態徽章
|
||||
const statusContainer = DOMUtils.createElement('div', { className: 'session-status' });
|
||||
const statusBadge = DOMUtils.createElement('span', {
|
||||
className: 'status-badge ' + (sessionData.status || 'waiting'),
|
||||
textContent: StatusUtils.getStatusText(sessionData.status)
|
||||
});
|
||||
|
||||
statusContainer.appendChild(statusBadge);
|
||||
header.appendChild(sessionId);
|
||||
header.appendChild(statusContainer);
|
||||
|
||||
return header;
|
||||
};
|
||||
|
||||
/**
|
||||
* 創建會話資訊區域
|
||||
*/
|
||||
SessionUIRenderer.prototype.createSessionInfo = function(sessionData, isHistory) {
|
||||
const info = DOMUtils.createElement('div', { className: 'session-info' });
|
||||
|
||||
// 時間資訊
|
||||
const timeText = sessionData.created_at ?
|
||||
TimeUtils.formatTimestamp(sessionData.created_at, { format: 'time' }) :
|
||||
'--:--:--';
|
||||
|
||||
const timeElement = DOMUtils.createElement('div', {
|
||||
className: 'session-time',
|
||||
textContent: (isHistory ? '完成時間' : '建立時間') + ': ' + timeText
|
||||
});
|
||||
|
||||
info.appendChild(timeElement);
|
||||
|
||||
// 歷史會話顯示持續時間
|
||||
if (isHistory) {
|
||||
const duration = this.calculateDisplayDuration(sessionData);
|
||||
const durationElement = DOMUtils.createElement('div', {
|
||||
className: 'session-duration',
|
||||
textContent: '持續時間: ' + duration
|
||||
});
|
||||
info.appendChild(durationElement);
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
/**
|
||||
* 計算顯示用的持續時間
|
||||
*/
|
||||
SessionUIRenderer.prototype.calculateDisplayDuration = function(sessionData) {
|
||||
if (sessionData.duration && sessionData.duration > 0) {
|
||||
return TimeUtils.formatDuration(sessionData.duration);
|
||||
} else if (sessionData.created_at && sessionData.completed_at) {
|
||||
const duration = sessionData.completed_at - sessionData.created_at;
|
||||
return TimeUtils.formatDuration(duration);
|
||||
} else if (sessionData.created_at) {
|
||||
return TimeUtils.estimateSessionDuration(sessionData);
|
||||
}
|
||||
return '未知';
|
||||
};
|
||||
|
||||
/**
|
||||
* 創建會話操作區域
|
||||
*/
|
||||
SessionUIRenderer.prototype.createSessionActions = function(sessionData, isHistory) {
|
||||
const actions = DOMUtils.createElement('div', { className: 'session-actions' });
|
||||
|
||||
const button = DOMUtils.createElement('button', {
|
||||
className: 'btn-small',
|
||||
textContent: isHistory ? '查看' : '詳細資訊'
|
||||
});
|
||||
|
||||
// 添加點擊事件
|
||||
DOMUtils.addEventListener(button, 'click', function() {
|
||||
if (window.MCPFeedback && window.MCPFeedback.SessionManager) {
|
||||
window.MCPFeedback.SessionManager.viewSessionDetails(sessionData.session_id);
|
||||
}
|
||||
});
|
||||
|
||||
actions.appendChild(button);
|
||||
return actions;
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染統計資訊
|
||||
*/
|
||||
SessionUIRenderer.prototype.renderStats = function(stats) {
|
||||
console.log('🎨 渲染統計資訊:', stats);
|
||||
|
||||
// 更新今日會話數
|
||||
if (this.statsElements.todayCount) {
|
||||
DOMUtils.safeSetTextContent(this.statsElements.todayCount, stats.todayCount.toString());
|
||||
}
|
||||
|
||||
// 更新平均時長
|
||||
if (this.statsElements.averageDuration) {
|
||||
const durationText = TimeUtils.formatDuration(stats.averageDuration);
|
||||
DOMUtils.safeSetTextContent(this.statsElements.averageDuration, durationText);
|
||||
}
|
||||
|
||||
// 更新總會話數
|
||||
if (this.statsElements.totalSessions) {
|
||||
DOMUtils.safeSetTextContent(this.statsElements.totalSessions, stats.totalSessions.toString());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加載入動畫
|
||||
*/
|
||||
SessionUIRenderer.prototype.showLoading = function(element) {
|
||||
if (element && this.enableAnimations) {
|
||||
DOMUtils.safeAddClass(element, 'loading');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除載入動畫
|
||||
*/
|
||||
SessionUIRenderer.prototype.hideLoading = function(element) {
|
||||
if (element && this.enableAnimations) {
|
||||
DOMUtils.safeRemoveClass(element, 'loading');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 啟動活躍時間定時器
|
||||
*/
|
||||
SessionUIRenderer.prototype.startActiveTimeTimer = function() {
|
||||
const self = this;
|
||||
|
||||
// 清除現有定時器
|
||||
if (this.activeTimeTimer) {
|
||||
clearInterval(this.activeTimeTimer);
|
||||
}
|
||||
|
||||
// 每秒更新活躍時間
|
||||
this.activeTimeTimer = setInterval(function() {
|
||||
self.updateActiveTime();
|
||||
}, 1000);
|
||||
|
||||
console.log('🎨 活躍時間定時器已啟動');
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止活躍時間定時器
|
||||
*/
|
||||
SessionUIRenderer.prototype.stopActiveTimeTimer = function() {
|
||||
if (this.activeTimeTimer) {
|
||||
clearInterval(this.activeTimeTimer);
|
||||
this.activeTimeTimer = null;
|
||||
console.log('🎨 活躍時間定時器已停止');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置活躍時間定時器
|
||||
*/
|
||||
SessionUIRenderer.prototype.resetActiveTimeTimer = function() {
|
||||
this.stopActiveTimeTimer();
|
||||
this.startActiveTimeTimer();
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新活躍時間顯示
|
||||
*/
|
||||
SessionUIRenderer.prototype.updateActiveTime = function() {
|
||||
if (!this.currentSessionData || !this.currentSessionData.created_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTimeElement = document.getElementById('sessionAge');
|
||||
if (activeTimeElement) {
|
||||
const timeText = TimeUtils.formatElapsedTime(this.currentSessionData.created_at);
|
||||
DOMUtils.safeSetTextContent(activeTimeElement, timeText);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理資源
|
||||
*/
|
||||
SessionUIRenderer.prototype.cleanup = function() {
|
||||
// 停止定時器
|
||||
this.stopActiveTimeTimer();
|
||||
|
||||
// 清理引用
|
||||
this.currentSessionCard = null;
|
||||
this.historyList = null;
|
||||
this.statsElements = {};
|
||||
this.currentSessionData = null;
|
||||
|
||||
console.log('🎨 SessionUIRenderer 清理完成');
|
||||
};
|
||||
|
||||
// 將 SessionUIRenderer 加入命名空間
|
||||
window.MCPFeedback.Session.UIRenderer = SessionUIRenderer;
|
||||
|
||||
console.log('✅ SessionUIRenderer 模組載入完成');
|
||||
|
||||
})();
|
@ -0,0 +1,302 @@
|
||||
/**
|
||||
* MCP Feedback Enhanced - DOM 操作工具模組
|
||||
* ==========================================
|
||||
*
|
||||
* 提供通用的 DOM 操作和元素管理功能
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 確保命名空間存在
|
||||
window.MCPFeedback = window.MCPFeedback || {};
|
||||
window.MCPFeedback.Utils = window.MCPFeedback.Utils || {};
|
||||
|
||||
/**
|
||||
* DOM 工具類
|
||||
*/
|
||||
const DOMUtils = {
|
||||
/**
|
||||
* 安全查詢選擇器
|
||||
*/
|
||||
safeQuerySelector: function(selector) {
|
||||
try {
|
||||
return document.querySelector(selector);
|
||||
} catch (error) {
|
||||
console.warn('查詢選擇器失敗:', selector, error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 安全查詢所有選擇器
|
||||
*/
|
||||
safeQuerySelectorAll: function(selector) {
|
||||
try {
|
||||
return document.querySelectorAll(selector);
|
||||
} catch (error) {
|
||||
console.warn('查詢所有選擇器失敗:', selector, error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 安全設置文本內容
|
||||
*/
|
||||
safeSetTextContent: function(element, text) {
|
||||
if (element && typeof element.textContent !== 'undefined') {
|
||||
element.textContent = text || '';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 安全設置 HTML 內容
|
||||
*/
|
||||
safeSetInnerHTML: function(element, html) {
|
||||
if (element && typeof element.innerHTML !== 'undefined') {
|
||||
element.innerHTML = html || '';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 安全添加 CSS 類
|
||||
*/
|
||||
safeAddClass: function(element, className) {
|
||||
if (element && element.classList && className) {
|
||||
element.classList.add(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 安全移除 CSS 類
|
||||
*/
|
||||
safeRemoveClass: function(element, className) {
|
||||
if (element && element.classList && className) {
|
||||
element.classList.remove(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 安全切換 CSS 類
|
||||
*/
|
||||
safeToggleClass: function(element, className) {
|
||||
if (element && element.classList && className) {
|
||||
element.classList.toggle(className);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查元素是否包含指定類
|
||||
*/
|
||||
hasClass: function(element, className) {
|
||||
return element && element.classList && element.classList.contains(className);
|
||||
},
|
||||
|
||||
/**
|
||||
* 創建元素
|
||||
*/
|
||||
createElement: function(tagName, options) {
|
||||
options = options || {};
|
||||
const element = document.createElement(tagName);
|
||||
|
||||
if (options.className) {
|
||||
element.className = options.className;
|
||||
}
|
||||
|
||||
if (options.id) {
|
||||
element.id = options.id;
|
||||
}
|
||||
|
||||
if (options.textContent) {
|
||||
element.textContent = options.textContent;
|
||||
}
|
||||
|
||||
if (options.innerHTML) {
|
||||
element.innerHTML = options.innerHTML;
|
||||
}
|
||||
|
||||
if (options.attributes) {
|
||||
Object.keys(options.attributes).forEach(function(key) {
|
||||
element.setAttribute(key, options.attributes[key]);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.styles) {
|
||||
Object.keys(options.styles).forEach(function(key) {
|
||||
element.style[key] = options.styles[key];
|
||||
});
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* 安全移除元素
|
||||
*/
|
||||
safeRemoveElement: function(element) {
|
||||
if (element && element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空元素內容
|
||||
*/
|
||||
clearElement: function(element) {
|
||||
if (element) {
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 顯示元素
|
||||
*/
|
||||
showElement: function(element) {
|
||||
if (element) {
|
||||
element.style.display = '';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 隱藏元素
|
||||
*/
|
||||
hideElement: function(element) {
|
||||
if (element) {
|
||||
element.style.display = 'none';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 切換元素顯示狀態
|
||||
*/
|
||||
toggleElement: function(element) {
|
||||
if (element) {
|
||||
const isHidden = element.style.display === 'none' ||
|
||||
window.getComputedStyle(element).display === 'none';
|
||||
if (isHidden) {
|
||||
this.showElement(element);
|
||||
} else {
|
||||
this.hideElement(element);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 設置元素屬性
|
||||
*/
|
||||
setAttribute: function(element, name, value) {
|
||||
if (element && name) {
|
||||
element.setAttribute(name, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取元素屬性
|
||||
*/
|
||||
getAttribute: function(element, name) {
|
||||
if (element && name) {
|
||||
return element.getAttribute(name);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除元素屬性
|
||||
*/
|
||||
removeAttribute: function(element, name) {
|
||||
if (element && name) {
|
||||
element.removeAttribute(name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加事件監聽器
|
||||
*/
|
||||
addEventListener: function(element, event, handler, options) {
|
||||
if (element && event && typeof handler === 'function') {
|
||||
element.addEventListener(event, handler, options);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除事件監聽器
|
||||
*/
|
||||
removeEventListener: function(element, event, handler, options) {
|
||||
if (element && event && typeof handler === 'function') {
|
||||
element.removeEventListener(event, handler, options);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取元素的邊界矩形
|
||||
*/
|
||||
getBoundingRect: function(element) {
|
||||
if (element && typeof element.getBoundingClientRect === 'function') {
|
||||
return element.getBoundingClientRect();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查元素是否在視窗內
|
||||
*/
|
||||
isElementInViewport: function(element) {
|
||||
const rect = this.getBoundingRect(element);
|
||||
if (!rect) return false;
|
||||
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 滾動到元素
|
||||
*/
|
||||
scrollToElement: function(element, options) {
|
||||
if (element && typeof element.scrollIntoView === 'function') {
|
||||
element.scrollIntoView(options || { behavior: 'smooth', block: 'center' });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 將 DOMUtils 加入命名空間
|
||||
window.MCPFeedback.Utils.DOM = DOMUtils;
|
||||
|
||||
console.log('✅ DOMUtils 模組載入完成');
|
||||
|
||||
})();
|
@ -0,0 +1,314 @@
|
||||
/**
|
||||
* MCP Feedback Enhanced - 狀態處理工具模組
|
||||
* ========================================
|
||||
*
|
||||
* 提供狀態映射、顏色管理和狀態轉換功能
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 確保命名空間存在
|
||||
window.MCPFeedback = window.MCPFeedback || {};
|
||||
window.MCPFeedback.Utils = window.MCPFeedback.Utils || {};
|
||||
|
||||
/**
|
||||
* 狀態工具類
|
||||
*/
|
||||
const StatusUtils = {
|
||||
/**
|
||||
* 會話狀態映射
|
||||
*/
|
||||
SESSION_STATUS_MAP: {
|
||||
'waiting': '等待回饋',
|
||||
'waiting_for_feedback': '等待回饋',
|
||||
'active': '進行中',
|
||||
'feedback_submitted': '已提交回饋',
|
||||
'completed': '已完成',
|
||||
'timeout': '已逾時',
|
||||
'error': '錯誤',
|
||||
'expired': '已過期',
|
||||
'connecting': '連接中',
|
||||
'connected': '已連接',
|
||||
'disconnected': '已斷開',
|
||||
'processing': '處理中',
|
||||
'ready': '就緒',
|
||||
'closed': '已關閉'
|
||||
},
|
||||
|
||||
/**
|
||||
* 連線狀態映射
|
||||
*/
|
||||
CONNECTION_STATUS_MAP: {
|
||||
'connecting': '連接中',
|
||||
'connected': '已連接',
|
||||
'disconnected': '已斷開',
|
||||
'reconnecting': '重連中',
|
||||
'error': '連接錯誤'
|
||||
},
|
||||
|
||||
/**
|
||||
* 狀態顏色映射
|
||||
*/
|
||||
STATUS_COLOR_MAP: {
|
||||
'waiting': '#9c27b0',
|
||||
'waiting_for_feedback': '#9c27b0',
|
||||
'active': '#2196f3',
|
||||
'feedback_submitted': '#4caf50',
|
||||
'completed': '#4caf50',
|
||||
'timeout': '#ff5722',
|
||||
'error': '#f44336',
|
||||
'expired': '#757575',
|
||||
'connecting': '#ff9800',
|
||||
'connected': '#4caf50',
|
||||
'disconnected': '#757575',
|
||||
'reconnecting': '#9c27b0',
|
||||
'processing': '#2196f3',
|
||||
'ready': '#4caf50',
|
||||
'closed': '#757575'
|
||||
},
|
||||
|
||||
/**
|
||||
* 連線品質等級
|
||||
*/
|
||||
CONNECTION_QUALITY_LEVELS: {
|
||||
'excellent': { threshold: 50, label: '優秀', color: '#4caf50' },
|
||||
'good': { threshold: 100, label: '良好', color: '#8bc34a' },
|
||||
'fair': { threshold: 200, label: '一般', color: '#ff9800' },
|
||||
'poor': { threshold: Infinity, label: '較差', color: '#f44336' }
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取狀態文字
|
||||
*/
|
||||
getStatusText: function(status) {
|
||||
if (!status) return '未知';
|
||||
return this.SESSION_STATUS_MAP[status] || this.CONNECTION_STATUS_MAP[status] || status;
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取狀態顏色
|
||||
*/
|
||||
getStatusColor: function(status) {
|
||||
if (!status) return '#757575';
|
||||
return this.STATUS_COLOR_MAP[status] || '#757575';
|
||||
},
|
||||
|
||||
/**
|
||||
* 根據延遲計算連線品質
|
||||
*/
|
||||
calculateConnectionQuality: function(latency) {
|
||||
if (typeof latency !== 'number' || latency < 0) {
|
||||
return { level: 'unknown', label: '未知', color: '#757575' };
|
||||
}
|
||||
|
||||
for (const [level, config] of Object.entries(this.CONNECTION_QUALITY_LEVELS)) {
|
||||
if (latency < config.threshold) {
|
||||
return {
|
||||
level: level,
|
||||
label: config.label,
|
||||
color: config.color
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { level: 'poor', label: '較差', color: '#f44336' };
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取信號強度等級(基於連線品質)
|
||||
*/
|
||||
getSignalStrength: function(quality) {
|
||||
const strengthMap = {
|
||||
'excellent': 3,
|
||||
'good': 2,
|
||||
'fair': 1,
|
||||
'poor': 0,
|
||||
'unknown': 0
|
||||
};
|
||||
|
||||
return strengthMap[quality] || 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查狀態是否為已完成狀態
|
||||
*/
|
||||
isCompletedStatus: function(status) {
|
||||
const completedStatuses = [
|
||||
'completed',
|
||||
'feedback_submitted',
|
||||
'timeout',
|
||||
'error',
|
||||
'expired',
|
||||
'closed'
|
||||
];
|
||||
return completedStatuses.includes(status);
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查狀態是否為活躍狀態
|
||||
*/
|
||||
isActiveStatus: function(status) {
|
||||
const activeStatuses = [
|
||||
'waiting',
|
||||
'waiting_for_feedback',
|
||||
'active',
|
||||
'processing',
|
||||
'connected',
|
||||
'ready'
|
||||
];
|
||||
return activeStatuses.includes(status);
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查狀態是否為錯誤狀態
|
||||
*/
|
||||
isErrorStatus: function(status) {
|
||||
const errorStatuses = ['error', 'timeout', 'disconnected'];
|
||||
return errorStatuses.includes(status);
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查狀態是否為連接中狀態
|
||||
*/
|
||||
isConnectingStatus: function(status) {
|
||||
const connectingStatuses = ['connecting', 'reconnecting'];
|
||||
return connectingStatuses.includes(status);
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取狀態優先級(用於排序)
|
||||
*/
|
||||
getStatusPriority: function(status) {
|
||||
const priorityMap = {
|
||||
'error': 1,
|
||||
'timeout': 2,
|
||||
'disconnected': 3,
|
||||
'connecting': 4,
|
||||
'reconnecting': 5,
|
||||
'waiting': 6,
|
||||
'waiting_for_feedback': 6,
|
||||
'processing': 7,
|
||||
'active': 8,
|
||||
'ready': 9,
|
||||
'connected': 10,
|
||||
'feedback_submitted': 11,
|
||||
'completed': 12,
|
||||
'closed': 13,
|
||||
'expired': 14
|
||||
};
|
||||
|
||||
return priorityMap[status] || 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 創建狀態徽章 HTML
|
||||
*/
|
||||
createStatusBadge: function(status, options) {
|
||||
options = options || {};
|
||||
const text = this.getStatusText(status);
|
||||
const color = this.getStatusColor(status);
|
||||
const className = options.className || 'status-badge';
|
||||
|
||||
return `<span class="${className} ${status}" style="color: ${color};">${text}</span>`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新狀態指示器
|
||||
*/
|
||||
updateStatusIndicator: function(element, status, options) {
|
||||
if (!element) return false;
|
||||
|
||||
options = options || {};
|
||||
const text = this.getStatusText(status);
|
||||
const color = this.getStatusColor(status);
|
||||
|
||||
// 更新文字
|
||||
if (options.updateText !== false) {
|
||||
element.textContent = text;
|
||||
}
|
||||
|
||||
// 更新顏色
|
||||
if (options.updateColor !== false) {
|
||||
element.style.color = color;
|
||||
}
|
||||
|
||||
// 更新 CSS 類
|
||||
if (options.updateClass !== false) {
|
||||
// 移除舊的狀態類
|
||||
element.className = element.className.replace(/\b(waiting|active|completed|error|connecting|connected|disconnected|reconnecting|processing|ready|closed|expired|timeout|feedback_submitted)\b/g, '');
|
||||
// 添加新的狀態類
|
||||
element.classList.add(status);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化狀態變更日誌
|
||||
*/
|
||||
formatStatusChangeLog: function(oldStatus, newStatus, timestamp) {
|
||||
const oldText = this.getStatusText(oldStatus);
|
||||
const newText = this.getStatusText(newStatus);
|
||||
const timeStr = timestamp ? new Date(timestamp).toLocaleTimeString() : '現在';
|
||||
|
||||
return `${timeStr}: ${oldText} → ${newText}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查狀態轉換是否有效
|
||||
*/
|
||||
isValidStatusTransition: function(fromStatus, toStatus) {
|
||||
// 定義有效的狀態轉換規則
|
||||
const validTransitions = {
|
||||
'waiting': ['active', 'processing', 'timeout', 'error', 'connected'],
|
||||
'waiting_for_feedback': ['active', 'processing', 'timeout', 'error', 'feedback_submitted'],
|
||||
'active': ['processing', 'feedback_submitted', 'completed', 'timeout', 'error'],
|
||||
'processing': ['completed', 'feedback_submitted', 'error', 'timeout'],
|
||||
'connecting': ['connected', 'error', 'disconnected', 'timeout'],
|
||||
'connected': ['disconnected', 'error', 'reconnecting'],
|
||||
'disconnected': ['connecting', 'reconnecting'],
|
||||
'reconnecting': ['connected', 'error', 'disconnected'],
|
||||
'feedback_submitted': ['completed', 'closed'],
|
||||
'completed': ['closed'],
|
||||
'error': ['connecting', 'waiting', 'closed'],
|
||||
'timeout': ['closed', 'waiting'],
|
||||
'ready': ['active', 'waiting', 'processing']
|
||||
};
|
||||
|
||||
const allowedTransitions = validTransitions[fromStatus];
|
||||
return allowedTransitions ? allowedTransitions.includes(toStatus) : true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取狀態描述
|
||||
*/
|
||||
getStatusDescription: function(status) {
|
||||
const descriptions = {
|
||||
'waiting': '系統正在等待用戶提供回饋',
|
||||
'waiting_for_feedback': '系統正在等待用戶提供回饋',
|
||||
'active': '會話正在進行中',
|
||||
'processing': '系統正在處理用戶的回饋',
|
||||
'feedback_submitted': '用戶已提交回饋',
|
||||
'completed': '會話已成功完成',
|
||||
'timeout': '會話因超時而結束',
|
||||
'error': '會話遇到錯誤',
|
||||
'expired': '會話已過期',
|
||||
'connecting': '正在建立連接',
|
||||
'connected': '連接已建立',
|
||||
'disconnected': '連接已斷開',
|
||||
'reconnecting': '正在嘗試重新連接',
|
||||
'ready': '系統已就緒',
|
||||
'closed': '會話已關閉'
|
||||
};
|
||||
|
||||
return descriptions[status] || '未知狀態';
|
||||
}
|
||||
};
|
||||
|
||||
// 將 StatusUtils 加入命名空間
|
||||
window.MCPFeedback.Utils.Status = StatusUtils;
|
||||
|
||||
console.log('✅ StatusUtils 模組載入完成');
|
||||
|
||||
})();
|
@ -0,0 +1,294 @@
|
||||
/**
|
||||
* MCP Feedback Enhanced - 時間處理工具模組
|
||||
* ========================================
|
||||
*
|
||||
* 提供時間格式化、計算和顯示功能
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 確保命名空間存在
|
||||
window.MCPFeedback = window.MCPFeedback || {};
|
||||
window.MCPFeedback.Utils = window.MCPFeedback.Utils || {};
|
||||
|
||||
/**
|
||||
* 時間工具類
|
||||
*/
|
||||
const TimeUtils = {
|
||||
/**
|
||||
* 格式化時間戳為可讀時間
|
||||
*/
|
||||
formatTimestamp: function(timestamp, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!timestamp) return '未知';
|
||||
|
||||
try {
|
||||
// 處理時間戳格式(毫秒轉秒)
|
||||
let normalizedTimestamp = timestamp;
|
||||
if (timestamp > 1e12) {
|
||||
normalizedTimestamp = timestamp / 1000;
|
||||
}
|
||||
|
||||
const date = new Date(normalizedTimestamp * 1000);
|
||||
if (isNaN(date.getTime())) {
|
||||
return '無效時間';
|
||||
}
|
||||
|
||||
if (options.format === 'time') {
|
||||
// 只返回時間部分
|
||||
return date.toLocaleTimeString();
|
||||
} else if (options.format === 'date') {
|
||||
// 只返回日期部分
|
||||
return date.toLocaleDateString();
|
||||
} else if (options.format === 'iso') {
|
||||
// ISO 格式
|
||||
return date.toISOString();
|
||||
} else {
|
||||
// 完整格式
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('時間格式化失敗:', timestamp, error);
|
||||
return '格式錯誤';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化持續時間(秒)
|
||||
*/
|
||||
formatDuration: function(seconds) {
|
||||
if (!seconds || seconds < 0) return '0秒';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小時${minutes > 0 ? minutes + '分鐘' : ''}`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分鐘${remainingSeconds > 0 ? remainingSeconds + '秒' : ''}`;
|
||||
} else {
|
||||
return `${remainingSeconds}秒`;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化相對時間(多久之前)
|
||||
*/
|
||||
formatRelativeTime: function(timestamp) {
|
||||
if (!timestamp) return '未知';
|
||||
|
||||
try {
|
||||
let normalizedTimestamp = timestamp;
|
||||
if (timestamp > 1e12) {
|
||||
normalizedTimestamp = timestamp / 1000;
|
||||
}
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
const diff = now - normalizedTimestamp;
|
||||
|
||||
if (diff < 60) {
|
||||
return '剛剛';
|
||||
} else if (diff < 3600) {
|
||||
const minutes = Math.floor(diff / 60);
|
||||
return `${minutes}分鐘前`;
|
||||
} else if (diff < 86400) {
|
||||
const hours = Math.floor(diff / 3600);
|
||||
return `${hours}小時前`;
|
||||
} else {
|
||||
const days = Math.floor(diff / 86400);
|
||||
return `${days}天前`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('相對時間計算失敗:', timestamp, error);
|
||||
return '計算錯誤';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 計算經過時間(從指定時間到現在)
|
||||
*/
|
||||
calculateElapsedTime: function(startTimestamp) {
|
||||
if (!startTimestamp) return 0;
|
||||
|
||||
try {
|
||||
let normalizedTimestamp = startTimestamp;
|
||||
if (startTimestamp > 1e12) {
|
||||
normalizedTimestamp = startTimestamp / 1000;
|
||||
}
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
return Math.max(0, now - normalizedTimestamp);
|
||||
} catch (error) {
|
||||
console.warn('經過時間計算失敗:', startTimestamp, error);
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化經過時間為 MM:SS 格式
|
||||
*/
|
||||
formatElapsedTime: function(startTimestamp) {
|
||||
const elapsed = this.calculateElapsedTime(startTimestamp);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = Math.floor(elapsed % 60);
|
||||
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取當前時間戳(秒)
|
||||
*/
|
||||
getCurrentTimestamp: function() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取當前時間戳(毫秒)
|
||||
*/
|
||||
getCurrentTimestampMs: function() {
|
||||
return Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查時間戳是否有效
|
||||
*/
|
||||
isValidTimestamp: function(timestamp) {
|
||||
if (!timestamp || typeof timestamp !== 'number') return false;
|
||||
|
||||
// 檢查是否在合理範圍內(1970年到2100年)
|
||||
const minTimestamp = 0;
|
||||
const maxTimestamp = 4102444800; // 2100年1月1日
|
||||
|
||||
let normalizedTimestamp = timestamp;
|
||||
if (timestamp > 1e12) {
|
||||
normalizedTimestamp = timestamp / 1000;
|
||||
}
|
||||
|
||||
return normalizedTimestamp >= minTimestamp && normalizedTimestamp <= maxTimestamp;
|
||||
},
|
||||
|
||||
/**
|
||||
* 標準化時間戳(統一轉換為秒)
|
||||
*/
|
||||
normalizeTimestamp: function(timestamp) {
|
||||
if (!this.isValidTimestamp(timestamp)) return null;
|
||||
|
||||
if (timestamp > 1e12) {
|
||||
return timestamp / 1000;
|
||||
}
|
||||
return timestamp;
|
||||
},
|
||||
|
||||
/**
|
||||
* 創建倒計時器
|
||||
*/
|
||||
createCountdown: function(endTimestamp, callback, options) {
|
||||
options = options || {};
|
||||
const interval = options.interval || 1000;
|
||||
|
||||
const timer = setInterval(function() {
|
||||
const now = Date.now() / 1000;
|
||||
const remaining = endTimestamp - now;
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(timer);
|
||||
if (callback) callback(0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback) callback(remaining, false);
|
||||
}, interval);
|
||||
|
||||
return timer;
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化倒計時顯示
|
||||
*/
|
||||
formatCountdown: function(remainingSeconds) {
|
||||
if (remainingSeconds <= 0) return '00:00';
|
||||
|
||||
const minutes = Math.floor(remainingSeconds / 60);
|
||||
const seconds = Math.floor(remainingSeconds % 60);
|
||||
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 獲取今天的開始時間戳
|
||||
*/
|
||||
getTodayStartTimestamp: function() {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return Math.floor(today.getTime() / 1000);
|
||||
},
|
||||
|
||||
/**
|
||||
* 檢查時間戳是否是今天
|
||||
*/
|
||||
isToday: function(timestamp) {
|
||||
if (!this.isValidTimestamp(timestamp)) return false;
|
||||
|
||||
const normalizedTimestamp = this.normalizeTimestamp(timestamp);
|
||||
const todayStart = this.getTodayStartTimestamp();
|
||||
const todayEnd = todayStart + 86400; // 24小時後
|
||||
|
||||
return normalizedTimestamp >= todayStart && normalizedTimestamp < todayEnd;
|
||||
},
|
||||
|
||||
/**
|
||||
* 估算會話持續時間(用於歷史會話)
|
||||
*/
|
||||
estimateSessionDuration: function(sessionData) {
|
||||
// 基礎時間 2 分鐘
|
||||
let estimatedMinutes = 2;
|
||||
|
||||
// 根據摘要長度調整
|
||||
if (sessionData.summary) {
|
||||
const summaryLength = sessionData.summary.length;
|
||||
if (summaryLength > 100) {
|
||||
estimatedMinutes += Math.floor(summaryLength / 50);
|
||||
}
|
||||
}
|
||||
|
||||
// 根據會話 ID 的哈希值增加隨機性
|
||||
if (sessionData.session_id) {
|
||||
const hash = this.simpleHash(sessionData.session_id);
|
||||
const variation = (hash % 5) + 1; // 1-5 分鐘的變化
|
||||
estimatedMinutes += variation;
|
||||
}
|
||||
|
||||
// 限制在合理範圍內
|
||||
estimatedMinutes = Math.max(1, Math.min(estimatedMinutes, 15));
|
||||
|
||||
return `約 ${estimatedMinutes} 分鐘`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 簡單哈希函數
|
||||
*/
|
||||
simpleHash: function(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // 轉換為 32 位整數
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
};
|
||||
|
||||
// 將 TimeUtils 加入命名空間
|
||||
window.MCPFeedback.Utils.Time = TimeUtils;
|
||||
|
||||
console.log('✅ TimeUtils 模組載入完成');
|
||||
|
||||
})();
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
function WebSocketManager(options) {
|
||||
options = options || {};
|
||||
|
||||
|
||||
this.websocket = null;
|
||||
this.isConnected = false;
|
||||
this.connectionReady = false;
|
||||
@ -26,17 +26,20 @@
|
||||
this.reconnectDelay = options.reconnectDelay || Utils.CONSTANTS.DEFAULT_RECONNECT_DELAY;
|
||||
this.heartbeatInterval = null;
|
||||
this.heartbeatFrequency = options.heartbeatFrequency || Utils.CONSTANTS.DEFAULT_HEARTBEAT_FREQUENCY;
|
||||
|
||||
|
||||
// 事件回調
|
||||
this.onOpen = options.onOpen || null;
|
||||
this.onMessage = options.onMessage || null;
|
||||
this.onClose = options.onClose || null;
|
||||
this.onError = options.onError || null;
|
||||
this.onConnectionStatusChange = options.onConnectionStatusChange || null;
|
||||
|
||||
|
||||
// 標籤頁管理器引用
|
||||
this.tabManager = options.tabManager || null;
|
||||
|
||||
|
||||
// 連線監控器引用
|
||||
this.connectionMonitor = options.connectionMonitor || null;
|
||||
|
||||
// 待處理的提交
|
||||
this.pendingSubmission = null;
|
||||
this.sessionUpdatePending = false;
|
||||
@ -111,6 +114,11 @@
|
||||
this.reconnectAttempts = 0;
|
||||
this.reconnectDelay = Utils.CONSTANTS.DEFAULT_RECONNECT_DELAY;
|
||||
|
||||
// 通知連線監控器
|
||||
if (this.connectionMonitor) {
|
||||
this.connectionMonitor.startMonitoring();
|
||||
}
|
||||
|
||||
// 開始心跳
|
||||
this.startHeartbeat();
|
||||
|
||||
@ -130,8 +138,13 @@
|
||||
try {
|
||||
const data = Utils.safeJsonParse(event.data, null);
|
||||
if (data) {
|
||||
// 記錄訊息到監控器
|
||||
if (this.connectionMonitor) {
|
||||
this.connectionMonitor.recordMessage();
|
||||
}
|
||||
|
||||
this.processMessage(data);
|
||||
|
||||
|
||||
// 調用外部回調
|
||||
if (this.onMessage) {
|
||||
this.onMessage(data);
|
||||
@ -153,6 +166,11 @@
|
||||
// 停止心跳
|
||||
this.stopHeartbeat();
|
||||
|
||||
// 通知連線監控器
|
||||
if (this.connectionMonitor) {
|
||||
this.connectionMonitor.stopMonitoring();
|
||||
}
|
||||
|
||||
// 處理不同的關閉原因
|
||||
if (event.code === 4004) {
|
||||
this.updateConnectionStatus('disconnected', '沒有活躍會話');
|
||||
@ -198,7 +216,10 @@
|
||||
this.reconnectAttempts++;
|
||||
this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 15000);
|
||||
console.log(this.reconnectDelay / 1000 + '秒後嘗試重連... (第' + this.reconnectAttempts + '次)');
|
||||
|
||||
|
||||
// 更新狀態為重連中
|
||||
this.updateConnectionStatus('reconnecting', '重連中... (第' + this.reconnectAttempts + '次)');
|
||||
|
||||
const self = this;
|
||||
setTimeout(function() {
|
||||
console.log('🔄 開始重連 WebSocket... (第' + self.reconnectAttempts + '次)');
|
||||
@ -224,6 +245,10 @@
|
||||
break;
|
||||
case 'heartbeat_response':
|
||||
this.handleHeartbeatResponse();
|
||||
// 記錄 pong 時間到監控器
|
||||
if (this.connectionMonitor) {
|
||||
this.connectionMonitor.recordPong();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 其他訊息類型由外部處理
|
||||
@ -293,6 +318,11 @@
|
||||
const self = this;
|
||||
this.heartbeatInterval = setInterval(function() {
|
||||
if (self.websocket && self.websocket.readyState === WebSocket.OPEN) {
|
||||
// 記錄 ping 時間到監控器
|
||||
if (self.connectionMonitor) {
|
||||
self.connectionMonitor.recordPing();
|
||||
}
|
||||
|
||||
self.send({
|
||||
type: 'heartbeat',
|
||||
tabId: self.tabManager ? self.tabManager.getTabId() : null,
|
||||
|
@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
<link rel="stylesheet" href="/static/css/session-management.css">
|
||||
<style>
|
||||
/* 僅保留必要的頁面特定樣式和響應式調整 */
|
||||
|
||||
@ -362,28 +363,162 @@
|
||||
</style>
|
||||
</head>
|
||||
<body class="layout-{{ layout_mode }}">
|
||||
<div class="container">
|
||||
<!-- ===== 頁面頭部區域 ===== -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<h1 class="title" data-i18n="app.title">MCP Feedback Enhanced</h1>
|
||||
<!-- 倒數計時器顯示 -->
|
||||
<div id="countdownDisplay" class="countdown-display" style="display: none;">
|
||||
<span class="countdown-label" data-i18n="timeout.remaining">剩餘時間</span>
|
||||
<span id="countdownTimer" class="countdown-timer">--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-info">
|
||||
<span data-i18n="app.projectDirectory">專案目錄</span>: {{ project_directory }}
|
||||
<!-- ===== 頂部連線監控狀態列 ===== -->
|
||||
<div class="connection-monitor-bar">
|
||||
<!-- 左側:應用標題和專案資訊 -->
|
||||
<div class="app-info-section">
|
||||
<div class="app-title">
|
||||
<h1 data-i18n="app.title">MCP Feedback Enhanced</h1>
|
||||
<!-- 倒數計時器顯示 -->
|
||||
<div id="countdownDisplay" class="countdown-display" style="display: none;">
|
||||
<span class="countdown-label" data-i18n="timeout.remaining">剩餘時間</span>
|
||||
<span id="countdownTimer" class="countdown-timer">--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="project-info">
|
||||
<span data-i18n="app.projectDirectory">專案目錄</span>: {{ project_directory }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中間:連線狀態資訊 -->
|
||||
<div class="connection-status-group">
|
||||
<!-- 主要連線狀態 -->
|
||||
<div class="connection-indicator connecting" id="mainConnectionStatus">
|
||||
<div class="status-icon pulse"></div>
|
||||
<span class="status-text">連接中...</span>
|
||||
<div class="connection-quality">
|
||||
<div class="latency-indicator">延遲: --ms</div>
|
||||
<div class="signal-strength">
|
||||
<div class="signal-bar"></div>
|
||||
<div class="signal-bar"></div>
|
||||
<div class="signal-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 連線詳細資訊 -->
|
||||
<div class="connection-details">
|
||||
<span class="connection-time">連線時間: --:--</span>
|
||||
<span class="reconnect-count">重連: 0 次</span>
|
||||
</div>
|
||||
|
||||
<!-- 詳細狀態資訊 -->
|
||||
<div class="detailed-status-info">
|
||||
<div class="websocket-metrics">
|
||||
<span class="metric">訊息: <span id="messageCount">0</span></span>
|
||||
<span class="metric">延遲: <span id="latencyDisplay">--ms</span></span>
|
||||
</div>
|
||||
<div class="session-metrics">
|
||||
<span class="metric">會話: <span id="sessionCount">1</span></span>
|
||||
<span class="metric">狀態: <span id="sessionStatusText">等待中</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右側:保留空間以保持佈局平衡 -->
|
||||
<div class="quick-actions">
|
||||
<!-- 移除不必要的按鈕,保持佈局平衡 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
|
||||
<!-- ===== 主內容區域 ===== -->
|
||||
<main class="main-content">
|
||||
<!-- 分頁導航 -->
|
||||
<div class="tabs">
|
||||
<main class="main-content" style="display: flex; gap: 0;">
|
||||
<!-- ===== 左側會話管理面板 ===== -->
|
||||
<div class="session-management-panel" id="sessionPanel">
|
||||
<!-- 面板標題 -->
|
||||
<div class="panel-header">
|
||||
<h3>會話管理</h3>
|
||||
<div class="panel-controls">
|
||||
<button class="btn-icon" id="refreshSessions" title="重新整理">
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<!-- 當前活躍會話 -->
|
||||
<div class="current-session-section">
|
||||
<h4>當前會話</h4>
|
||||
<div class="session-card active" id="currentSessionCard">
|
||||
<div class="session-header">
|
||||
<div class="session-id">會話 ID: {{ session_id[:8] if session_id else 'loading' }}...</div>
|
||||
<div class="session-status">
|
||||
<span class="status-badge waiting">等待中</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-info">
|
||||
<div class="session-time">建立時間: --:--:--</div>
|
||||
<div class="session-project">專案: {{ project_directory }}</div>
|
||||
<div class="session-summary">AI 摘要: 載入中...</div>
|
||||
</div>
|
||||
<div class="session-actions">
|
||||
<button class="btn-small" id="viewSessionDetails">詳細資訊</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 會話歷史記錄 -->
|
||||
<div class="session-history-section">
|
||||
<h4>會話歷史</h4>
|
||||
<div class="session-list" id="sessionHistoryList">
|
||||
<div class="no-sessions">暫無歷史會話</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 會話統計 -->
|
||||
<div class="session-stats-section">
|
||||
<h4>統計資訊</h4>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-label">今日會話</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">--</div>
|
||||
<div class="stat-label">平均時長</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邊緣收合/展開按鈕 -->
|
||||
<div class="panel-edge-toggle" id="panelEdgeToggle">
|
||||
<button class="edge-toggle-btn" id="edgeToggleBtn" title="收合面板">
|
||||
<span class="toggle-icon">◀</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收合狀態下的展開按鈕 -->
|
||||
<div class="collapsed-panel-toggle" id="collapsedPanelToggle" style="display: none;">
|
||||
<button class="collapsed-toggle-btn" id="collapsedToggleBtn" title="展開會話面板">
|
||||
<span class="toggle-icon">▶</span>
|
||||
<span class="toggle-text">會話</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== 右側主要內容區域 ===== -->
|
||||
<div class="main-content-area" style="flex: 1; min-width: 0;">
|
||||
<!-- 會話狀態條 -->
|
||||
<div class="session-status-bar" style="display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); margin-bottom: 16px; border-radius: 6px;">
|
||||
<div class="current-session-info">
|
||||
<span class="session-indicator" style="display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-secondary);">
|
||||
📋 當前會話: <span id="currentSessionId" style="font-family: monospace; color: var(--accent-color);">{{ session_id[:8] if session_id else 'loading' }}...</span>
|
||||
</span>
|
||||
<span class="session-age" style="margin-left: 16px; font-size: 12px; color: var(--text-secondary);">活躍時間: <span id="sessionAge">--</span></span>
|
||||
</div>
|
||||
<div class="session-controls">
|
||||
<button class="btn-link" id="switchSessionBtn" style="display: none; font-size: 12px; color: var(--accent-color); background: none; border: none; cursor: pointer;">
|
||||
切換會話
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分頁導航 -->
|
||||
<div class="tabs">
|
||||
<div class="tab-buttons">
|
||||
<!-- 工作區分頁 - 移到最左邊第一個 -->
|
||||
<button class="tab-button hidden" data-tab="combined" data-i18n="tabs.combined">
|
||||
@ -732,6 +867,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- 關閉 main-content-area -->
|
||||
</main>
|
||||
|
||||
<!-- 底部操作按鈕 -->
|
||||
@ -748,9 +884,22 @@
|
||||
<!-- WebSocket 和 JavaScript -->
|
||||
<script src="/static/js/i18n.js?v=2025010510"></script>
|
||||
<!-- 載入所有模組 -->
|
||||
<!-- 工具模組 -->
|
||||
<script src="/static/js/modules/utils/dom-utils.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/utils/time-utils.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/utils/status-utils.js?v=2025010510"></script>
|
||||
|
||||
<!-- 會話管理模組 -->
|
||||
<script src="/static/js/modules/session/session-data-manager.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/session/session-ui-renderer.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/session/session-details-modal.js?v=2025010510"></script>
|
||||
|
||||
<!-- 其他模組 -->
|
||||
<script src="/static/js/modules/utils.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/tab-manager.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/websocket-manager.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/connection-monitor.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/session-manager.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/image-handler.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/settings-manager.js?v=2025010510"></script>
|
||||
<script src="/static/js/modules/ui-manager.js?v=2025010510"></script>
|
||||
@ -765,6 +914,8 @@
|
||||
// 檢查所有必要的模組是否已載入
|
||||
if (!window.MCPFeedback ||
|
||||
!window.MCPFeedback.Utils ||
|
||||
!window.MCPFeedback.ConnectionMonitor ||
|
||||
!window.MCPFeedback.SessionManager ||
|
||||
!window.MCPFeedback.FeedbackApp) {
|
||||
console.error('❌ 模組載入不完整,延遲初始化...');
|
||||
setTimeout(initializeApp, 100);
|
||||
@ -783,6 +934,11 @@
|
||||
// 初始化應用程式
|
||||
await window.feedbackApp.init();
|
||||
|
||||
// 設置全域引用,讓 SessionManager 可以被 HTML 中的 onclick 調用
|
||||
if (window.feedbackApp.sessionManager) {
|
||||
window.MCPFeedback.app = window.feedbackApp;
|
||||
}
|
||||
|
||||
console.log('✅ 應用程式初始化完成');
|
||||
} catch (error) {
|
||||
console.error('❌ 應用程式初始化失敗:', error);
|
||||
|
Loading…
x
Reference in New Issue
Block a user