增加會話管理功能

This commit is contained in:
Minidoracat 2025-06-13 05:48:08 +08:00
parent 5f4e128f6f
commit a257744bd1
13 changed files with 4136 additions and 31 deletions

View 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;
}
}

View File

@ -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);
}
/* 分頁樣式 */

View File

@ -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();
}

View File

@ -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 模組載入完成');
})();

View File

@ -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 (重構版) 模組載入完成');
})();

View File

@ -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 模組載入完成');
})();

View File

@ -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="關閉">&times;</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 模組載入完成');
})();

View File

@ -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 模組載入完成');
})();

View File

@ -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 模組載入完成');
})();

View File

@ -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 模組載入完成');
})();

View File

@ -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 模組載入完成');
})();

View File

@ -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,

View File

@ -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);