From 3ed1150642a21bb785aff9bf790a57f3825e3c3a Mon Sep 17 00:00:00 2001 From: Minidoracat Date: Sun, 15 Jun 2025 17:29:39 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=96=B0=E5=A2=9E=20AI=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=88=97=E8=A1=A8=E6=94=AF=E6=8F=B4=20markdown=20?= =?UTF-8?q?=E9=A1=AF=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mcp_feedback_enhanced/__main__.py | 69 ++++- src/mcp_feedback_enhanced/web/main.py | 37 ++- .../web/static/css/styles.css | 284 ++++++++++++++++++ .../web/static/js/app.js | 1 + .../web/static/js/modules/ui-manager.js | 57 +++- .../web/templates/feedback.html | 29 +- 6 files changed, 466 insertions(+), 11 deletions(-) diff --git a/src/mcp_feedback_enhanced/__main__.py b/src/mcp_feedback_enhanced/__main__.py index 4a2cff4..25742fa 100644 --- a/src/mcp_feedback_enhanced/__main__.py +++ b/src/mcp_feedback_enhanced/__main__.py @@ -136,9 +136,72 @@ def test_web_ui_simple(): print("🔧 創建測試會話...") with tempfile.TemporaryDirectory() as temp_dir: - created_session_id = manager.create_session( - temp_dir, "Web UI 測試 - 驗證基本功能" - ) + markdown_test_content = """# Web UI 測試 - Markdown 渲染功能 + +## 🎯 測試目標 +驗證 **combinedSummaryContent** 區域的 Markdown 語法顯示功能 + +### ✨ 支援的語法特性 + +#### 文字格式 +- **粗體文字** 使用雙星號 +- *斜體文字* 使用單星號 +- ~~刪除線文字~~ 使用雙波浪號 +- `行內程式碼` 使用反引號 + +#### 程式碼區塊 +```javascript +// JavaScript 範例 +function renderMarkdown(content) { + return marked.parse(content); +} +``` + +```python +# Python 範例 +def process_feedback(data): + return {"status": "success", "data": data} +``` + +#### 列表功能 +**無序列表:** +- 第一個項目 +- 第二個項目 + - 巢狀項目 1 + - 巢狀項目 2 +- 第三個項目 + +**有序列表:** +1. 初始化 Markdown 渲染器 +2. 載入 marked.js 和 DOMPurify +3. 配置安全選項 +4. 渲染內容 + +#### 連結和引用 +- 專案連結:[MCP Feedback Enhanced](https://github.com/example/mcp-feedback-enhanced) +- 文檔連結:[Marked.js 官方文檔](https://marked.js.org/) + +> **重要提示:** 所有 HTML 輸出都經過 DOMPurify 清理,確保安全性。 + +#### 表格範例 +| 功能 | 狀態 | 說明 | +|------|------|------| +| 標題渲染 | ✅ | 支援 H1-H6 | +| 程式碼高亮 | ✅ | 基本語法高亮 | +| 列表功能 | ✅ | 有序/無序列表 | +| 連結處理 | ✅ | 安全連結渲染 | + +--- + +### 🔒 安全特性 +- XSS 防護:使用 DOMPurify 清理 +- 白名單標籤:僅允許安全的 HTML 標籤 +- URL 驗證:限制允許的 URL 協議 + +### 📝 測試結果 +如果您能看到上述內容以正確的格式顯示,表示 Markdown 渲染功能運作正常!""" + + created_session_id = manager.create_session(temp_dir, markdown_test_content) if created_session_id: print("✅ 會話創建成功") diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index 43ad4a0..f05f62c 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -1149,7 +1149,42 @@ if __name__ == "__main__": async def main(): try: project_dir = os.getcwd() - summary = "這是一個測試摘要,用於驗證 Web UI 功能。" + summary = """# Markdown 功能測試 + +## 🎯 任務完成摘要 + +我已成功為 **mcp-feedback-enhanced** 專案實現了 Markdown 語法顯示功能! + +### ✅ 完成的功能 + +1. **標題支援** - 支援 H1 到 H6 標題 +2. **文字格式化** + - **粗體文字** 使用雙星號 + - *斜體文字* 使用單星號 + - `行內程式碼` 使用反引號 +3. **程式碼區塊** +4. **列表功能** + - 無序列表項目 + - 有序列表項目 + +### 📋 技術實作 + +```javascript +// 使用 marked.js 進行 Markdown 解析 +const renderedContent = this.renderMarkdownSafely(summary); +element.innerHTML = renderedContent; +``` + +### 🔗 相關連結 + +- [marked.js 官方文檔](https://marked.js.org/) +- [DOMPurify 安全清理](https://github.com/cure53/DOMPurify) + +> **注意**: 此功能包含 XSS 防護,使用 DOMPurify 進行 HTML 清理。 + +--- + +**測試狀態**: ✅ 功能正常運作""" from ..debug import debug_log diff --git a/src/mcp_feedback_enhanced/web/static/css/styles.css b/src/mcp_feedback_enhanced/web/static/css/styles.css index c7b3910..6c5c773 100644 --- a/src/mcp_feedback_enhanced/web/static/css/styles.css +++ b/src/mcp_feedback_enhanced/web/static/css/styles.css @@ -1624,3 +1624,287 @@ textarea:-ms-input-placeholder, color: var(--error-color); } +/* ===== Markdown 內容樣式 ===== */ + +/* AI 摘要區域的 Markdown 樣式 - 基於 marked.js 官方樣式 */ +/* 移除舊的衝突樣式,使用下方的 VSCode 風格樣式 */ + +/* Markdown 強調樣式 */ +#summaryContent strong, #combinedSummaryContent strong { + font-weight: 600; + color: var(--text-primary); +} + +#summaryContent em, #combinedSummaryContent em { + font-style: italic; + color: var(--text-primary); +} + +/* Markdown 刪除線樣式 */ +#summaryContent del, #summaryContent s, +#combinedSummaryContent del, #combinedSummaryContent s { + text-decoration: line-through; + color: var(--text-secondary); + opacity: 0.7; +} + +/* Markdown 程式碼樣式 - 基於 marked.js 官方樣式 */ +#summaryContent code, #combinedSummaryContent code { + padding: 0.2em 0.4em !important; + margin: 0 !important; + font-size: 85% !important; + background-color: rgba(175, 184, 193, 0.2) !important; + border-radius: 3px !important; + font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace !important; + color: var(--text-primary) !important; +} + +#summaryContent pre, #combinedSummaryContent pre { + font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace !important; + padding: 16px !important; + overflow: auto !important; + font-size: 85% !important; + line-height: 1.45 !important; + background-color: rgba(175, 184, 193, 0.1) !important; + border-radius: 3px !important; + margin: 16px 0 !important; + border: none !important; +} + +#summaryContent pre code, #combinedSummaryContent pre code { + background: none !important; + border: none !important; + padding: 0 !important; + margin: 0 !important; + font-size: 100% !important; + color: var(--text-primary) !important; + display: block !important; + overflow: auto !important; +} + +/* 移除舊的列表樣式,使用下方的 VSCode 風格樣式 */ + +/* Markdown 引用區塊樣式 */ +#summaryContent blockquote, #combinedSummaryContent blockquote { + border-left: 4px solid var(--accent-color); + margin: 1em 0; + padding: 0.5em 1em; + background: rgba(0, 122, 204, 0.05); + color: var(--text-secondary); + font-style: italic; +} + +/* Markdown 連結樣式 - 基於 marked.js 官方樣式 */ +#summaryContent a, #combinedSummaryContent a { + color: #0366d6; + text-decoration: none; +} + +#summaryContent a:hover, #combinedSummaryContent a:hover { + text-decoration: underline; +} + +/* Markdown 水平線樣式 - 基於 marked.js 官方樣式 */ +#summaryContent hr, #combinedSummaryContent hr { + border: none; + border-top: 1px solid var(--border-color); + margin: 24px 0; + height: 0; +} + +/* 確保第一個元素沒有上邊距 */ +#summaryContent > *:first-child, #combinedSummaryContent > *:first-child { + margin-top: 0; +} + +/* 確保最後一個元素沒有下邊距 */ +#summaryContent > *:last-child, #combinedSummaryContent > *:last-child { + margin-bottom: 0; +} + +/* Markdown 內容樣式 - VSCode 風格極緊湊樣式 */ +#summaryContent, #combinedSummaryContent { + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol" !important; + font-size: 14px !important; + line-height: 1.1 !important; + word-wrap: break-word !important; +} + +/* 強制覆蓋所有子元素的行高和間距 */ +#summaryContent *, #combinedSummaryContent * { + line-height: 1.1 !important; +} + +#summaryContent p, #combinedSummaryContent p { + margin: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + line-height: 1.1 !important; + font-size: 14px !important; + padding: 0 !important; +} + +/* 標題樣式 - VSCode 風格含底線 */ +#summaryContent h1, #summaryContent h2, #summaryContent h3, +#summaryContent h4, #summaryContent h5, #summaryContent h6, +#combinedSummaryContent h1, #combinedSummaryContent h2, #combinedSummaryContent h3, +#combinedSummaryContent h4, #combinedSummaryContent h5, #combinedSummaryContent h6 { + color: var(--text-primary) !important; + font-weight: 600 !important; +} + +#summaryContent h1, #combinedSummaryContent h1 { + margin: 0.3em 0 0.1em 0 !important; + line-height: 1.1 !important; + font-size: 1.8em !important; + border-bottom: 2px solid var(--border-color) !important; + padding-bottom: 6px !important; +} + +#summaryContent h2, #combinedSummaryContent h2 { + margin: 0.25em 0 0.08em 0 !important; + line-height: 1.1 !important; + font-size: 1.5em !important; + border-bottom: 1px solid var(--border-color) !important; + padding-bottom: 4px !important; +} + +#summaryContent h3, #combinedSummaryContent h3 { + margin: 0.2em 0 0.03em 0 !important; + line-height: 1.1 !important; + font-size: 1.3em !important; +} + +#summaryContent h4, #combinedSummaryContent h4 { + margin: 0.15em 0 0.02em 0 !important; + line-height: 1.1 !important; + font-size: 1.1em !important; +} + +#summaryContent h5, #combinedSummaryContent h5, +#summaryContent h6, #combinedSummaryContent h6 { + margin: 0.1em 0 0.02em 0 !important; + line-height: 1.1 !important; + font-size: 1em !important; +} + +/* 列表樣式 - VSCode 風格極緊湊 */ +#summaryContent ul, #combinedSummaryContent ul, +#summaryContent ol, #combinedSummaryContent ol { + margin: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + padding: 0 !important; + padding-left: 1.2em !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +#summaryContent li, #combinedSummaryContent li { + margin: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + line-height: 1.1 !important; + padding: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + font-size: 14px !important; +} + +#summaryContent li > ul, #combinedSummaryContent li > ul, +#summaryContent li > ol, #combinedSummaryContent li > ol { + margin: 0 !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + padding: 0 !important; + padding-left: 1.0em !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +/* 強制段落後的列表緊密貼合 */ +#summaryContent p + ul, #combinedSummaryContent p + ul, +#summaryContent p + ol, #combinedSummaryContent p + ol, +#summaryContent h1 + ul, #combinedSummaryContent h1 + ul, +#summaryContent h2 + ul, #combinedSummaryContent h2 + ul, +#summaryContent h3 + ul, #combinedSummaryContent h3 + ul, +#summaryContent h4 + ul, #combinedSummaryContent h4 + ul, +#summaryContent h1 + ol, #combinedSummaryContent h1 + ol, +#summaryContent h2 + ol, #combinedSummaryContent h2 + ol, +#summaryContent h3 + ol, #combinedSummaryContent h3 + ol, +#summaryContent h4 + ol, #combinedSummaryContent h4 + ol { + margin-top: 0 !important; + padding-top: 0 !important; +} + +#summaryContent blockquote, #combinedSummaryContent blockquote { + margin: 0.05em 0 !important; + padding: 0.1em 0.5em !important; + line-height: 1.1 !important; + font-size: 14px !important; +} + +#summaryContent pre, #combinedSummaryContent pre { + margin: 0.05em 0 !important; + line-height: 1.1 !important; + font-size: 13px !important; +} + +#summaryContent code, #combinedSummaryContent code { + line-height: 1.1 !important; + font-size: 13px !important; +} + +/* Markdown 表格樣式 - 適應主題顏色 */ +#summaryContent table, #combinedSummaryContent table { + border-spacing: 0 !important; + border-collapse: collapse !important; + display: table !important; + width: 100% !important; + margin: 0.5em 0 !important; + font-variant: tabular-nums !important; + border: 1px solid var(--border-color) !important; + border-radius: 6px !important; + overflow: hidden !important; + background-color: var(--bg-primary) !important; +} + +#summaryContent thead, #combinedSummaryContent thead { + display: table-header-group !important; +} + +#summaryContent tbody, #combinedSummaryContent tbody { + display: table-row-group !important; +} + +#summaryContent tr, #combinedSummaryContent tr { + display: table-row !important; + background-color: var(--bg-primary) !important; + border-top: 1px solid var(--border-color) !important; +} + +#summaryContent tr:nth-child(2n), #combinedSummaryContent tr:nth-child(2n) { + background-color: var(--bg-secondary) !important; +} + +#summaryContent td, #summaryContent th, +#combinedSummaryContent td, #combinedSummaryContent th { + display: table-cell !important; + padding: 4px 8px !important; + border: 1px solid var(--border-color) !important; + text-align: left !important; + vertical-align: top !important; + color: var(--text-color) !important; + line-height: 1.3 !important; +} + +#summaryContent th, #combinedSummaryContent th { + font-weight: 600 !important; + background-color: var(--bg-secondary) !important; + border-bottom: 2px solid var(--border-color) !important; +} + +#summaryContent td > *:last-child, #combinedSummaryContent td > *:last-child { + margin-bottom: 0 !important; +} + diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index 9b7e04b..a50b5bd 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -1222,6 +1222,7 @@ // 更新 AI 摘要內容 if (self.uiManager) { + // console.log('🔧 準備更新 AI 摘要內容,summary 長度:', sessionData.summary ? sessionData.summary.length : 'undefined'); self.uiManager.updateAISummaryContent(sessionData.summary); self.uiManager.resetFeedbackForm(); self.uiManager.updateStatusIndicator(); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js index ebd5bd4..966f197 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js @@ -418,22 +418,69 @@ } }; + /** + * 安全地渲染 Markdown 內容 + */ + UIManager.prototype.renderMarkdownSafely = function(content) { + try { + // 檢查 marked 和 DOMPurify 是否可用 + if (typeof window.marked === 'undefined' || typeof window.DOMPurify === 'undefined') { + console.warn('⚠️ Markdown 庫未載入,使用純文字顯示'); + return this.escapeHtml(content); + } + + // 使用 marked 解析 Markdown + const htmlContent = window.marked.parse(content); + + // 使用 DOMPurify 清理 HTML + const cleanHtml = window.DOMPurify.sanitize(htmlContent, { + ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'a', 'hr', 'del', 's', 'table', 'thead', 'tbody', 'tr', 'td', 'th'], + ALLOWED_ATTR: ['href', 'title', 'class', 'align', 'style'], + ALLOW_DATA_ATTR: false + }); + + return cleanHtml; + } catch (error) { + console.error('❌ Markdown 渲染失敗:', error); + return this.escapeHtml(content); + } + }; + + /** + * HTML 轉義函數 + */ + UIManager.prototype.escapeHtml = function(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }; + /** * 更新 AI 摘要內容 */ UIManager.prototype.updateAISummaryContent = function(summary) { - console.log('📝 更新 AI 摘要內容...'); + console.log('📝 更新 AI 摘要內容...', '內容長度:', summary ? summary.length : 'undefined'); + console.log('📝 marked 可用:', typeof window.marked !== 'undefined'); + console.log('📝 DOMPurify 可用:', typeof window.DOMPurify !== 'undefined'); + + // 渲染 Markdown 內容 + const renderedContent = this.renderMarkdownSafely(summary); + console.log('📝 渲染後內容長度:', renderedContent ? renderedContent.length : 'undefined'); const summaryContent = Utils.safeQuerySelector('#summaryContent'); if (summaryContent) { - summaryContent.textContent = summary; - console.log('✅ 已更新分頁模式摘要內容'); + summaryContent.innerHTML = renderedContent; + console.log('✅ 已更新分頁模式摘要內容(Markdown 渲染)'); + } else { + console.warn('⚠️ 找不到 #summaryContent 元素'); } const combinedSummaryContent = Utils.safeQuerySelector('#combinedSummaryContent'); if (combinedSummaryContent) { - combinedSummaryContent.textContent = summary; - console.log('✅ 已更新合併模式摘要內容'); + combinedSummaryContent.innerHTML = renderedContent; + console.log('✅ 已更新合併模式摘要內容(Markdown 渲染)'); + } else { + console.warn('⚠️ 找不到 #combinedSummaryContent 元素'); } }; diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html index 7f53574..df0c52e 100644 --- a/src/mcp_feedback_enhanced/web/templates/feedback.html +++ b/src/mcp_feedback_enhanced/web/templates/feedback.html @@ -563,7 +563,7 @@
-
+
{{ summary }}
@@ -607,7 +607,7 @@

📋 AI 工作摘要

-
{{ summary }}
+
{{ summary }}
@@ -1096,6 +1096,9 @@ + + + @@ -1140,6 +1143,14 @@ async function initializeApp() { const sessionId = '{{ session_id }}'; + // 檢查 Markdown 依賴庫 + if (typeof window.marked === 'undefined' || typeof window.DOMPurify === 'undefined') { + const logger = window.MCPFeedback?.logger || console; + logger.warn('Markdown 依賴庫尚未載入,等待中...'); + setTimeout(initializeApp, 100); + return; + } + // 檢查核心依賴 const requiredModules = [ 'MCPFeedback', @@ -1193,6 +1204,20 @@ window.MCPFeedback.app = window.feedbackApp; } + // 初始化完成後,立即渲染現有的 AI 摘要內容為 Markdown + setTimeout(function() { + if (window.feedbackApp && window.feedbackApp.uiManager) { + // 獲取當前的摘要內容 + const summaryElement = document.querySelector('#combinedSummaryContent'); + const summaryTabElement = document.querySelector('#summaryContent'); + + if (summaryElement && summaryElement.textContent) { + console.log('🔧 初始化時渲染 Markdown 內容...'); + window.feedbackApp.uiManager.updateAISummaryContent(summaryElement.textContent); + } + } + }, 100); + logger.info('應用程式初始化完成'); } catch (error) { const logger = window.MCPFeedback?.logger || console;