新增 AI 工作列表支援 markdown 顯示

This commit is contained in:
Minidoracat 2025-06-15 17:29:39 +08:00
parent d79b040162
commit 3ed1150642
6 changed files with 466 additions and 11 deletions

View File

@ -136,9 +136,72 @@ def test_web_ui_simple():
print("🔧 創建測試會話...") print("🔧 創建測試會話...")
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
created_session_id = manager.create_session( markdown_test_content = """# Web UI 測試 - Markdown 渲染功能
temp_dir, "Web UI 測試 - 驗證基本功能"
) ## 🎯 測試目標
驗證 **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: if created_session_id:
print("✅ 會話創建成功") print("✅ 會話創建成功")

View File

@ -1149,7 +1149,42 @@ if __name__ == "__main__":
async def main(): async def main():
try: try:
project_dir = os.getcwd() 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 from ..debug import debug_log

View File

@ -1624,3 +1624,287 @@ textarea:-ms-input-placeholder,
color: var(--error-color); 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;
}

View File

@ -1222,6 +1222,7 @@
// 更新 AI 摘要內容 // 更新 AI 摘要內容
if (self.uiManager) { if (self.uiManager) {
// console.log('🔧 準備更新 AI 摘要內容summary 長度:', sessionData.summary ? sessionData.summary.length : 'undefined');
self.uiManager.updateAISummaryContent(sessionData.summary); self.uiManager.updateAISummaryContent(sessionData.summary);
self.uiManager.resetFeedbackForm(); self.uiManager.resetFeedbackForm();
self.uiManager.updateStatusIndicator(); self.uiManager.updateStatusIndicator();

View File

@ -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 摘要內容 * 更新 AI 摘要內容
*/ */
UIManager.prototype.updateAISummaryContent = function(summary) { 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'); const summaryContent = Utils.safeQuerySelector('#summaryContent');
if (summaryContent) { if (summaryContent) {
summaryContent.textContent = summary; summaryContent.innerHTML = renderedContent;
console.log('✅ 已更新分頁模式摘要內容'); console.log('✅ 已更新分頁模式摘要內容Markdown 渲染)');
} else {
console.warn('⚠️ 找不到 #summaryContent 元素');
} }
const combinedSummaryContent = Utils.safeQuerySelector('#combinedSummaryContent'); const combinedSummaryContent = Utils.safeQuerySelector('#combinedSummaryContent');
if (combinedSummaryContent) { if (combinedSummaryContent) {
combinedSummaryContent.textContent = summary; combinedSummaryContent.innerHTML = renderedContent;
console.log('✅ 已更新合併模式摘要內容'); console.log('✅ 已更新合併模式摘要內容Markdown 渲染)');
} else {
console.warn('⚠️ 找不到 #combinedSummaryContent 元素');
} }
}; };

View File

@ -563,7 +563,7 @@
</div> </div>
<div class="input-group"> <div class="input-group">
<div id="summaryContent" class="text-input" style="min-height: 300px; white-space: pre-wrap !important; cursor: text; padding: 12px; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word;" data-dynamic-content="aiSummary"> <div id="summaryContent" class="text-input" style="min-height: 300px; cursor: text; padding: 12px; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word;" data-dynamic-content="aiSummary">
{{ summary }} {{ summary }}
</div> </div>
</div> </div>
@ -607,7 +607,7 @@
<h3 class="combined-section-title" data-i18n="combined.summaryTitle">📋 AI 工作摘要</h3> <h3 class="combined-section-title" data-i18n="combined.summaryTitle">📋 AI 工作摘要</h3>
</div> </div>
<div class="combined-summary"> <div class="combined-summary">
<div id="combinedSummaryContent" class="text-input" style="min-height: 200px; white-space: pre-wrap !important; cursor: text; padding: 12px; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word;" data-dynamic-content="aiSummary">{{ summary }}</div> <div id="combinedSummaryContent" class="text-input" style="min-height: 200px; cursor: text; padding: 12px; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word;" data-dynamic-content="aiSummary">{{ summary }}</div>
</div> </div>
</div> </div>
@ -1096,6 +1096,9 @@
</div> </div>
<!-- WebSocket 和 JavaScript --> <!-- WebSocket 和 JavaScript -->
<!-- Markdown 支援庫 -->
<script src="https://cdn.jsdelivr.net/npm/marked@14.1.3/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.2/dist/purify.min.js"></script>
<script src="/static/js/i18n.js?v=2025010510"></script> <script src="/static/js/i18n.js?v=2025010510"></script>
<!-- 載入所有模組 --> <!-- 載入所有模組 -->
<!-- 核心模組(最先載入) --> <!-- 核心模組(最先載入) -->
@ -1140,6 +1143,14 @@
async function initializeApp() { async function initializeApp() {
const sessionId = '{{ session_id }}'; 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 = [ const requiredModules = [
'MCPFeedback', 'MCPFeedback',
@ -1193,6 +1204,20 @@
window.MCPFeedback.app = window.feedbackApp; 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('應用程式初始化完成'); logger.info('應用程式初始化完成');
} catch (error) { } catch (error) {
const logger = window.MCPFeedback?.logger || console; const logger = window.MCPFeedback?.logger || console;