diff --git a/css/style.css b/css/style.css index 5cce7a8..f0726b0 100644 --- a/css/style.css +++ b/css/style.css @@ -39,6 +39,111 @@ body { box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3); } +/* Markdown样式 */ +.chat-bubble-ai h1 { + font-size: 1.5em; + font-weight: bold; + margin: 0.5em 0; + color: #1f2937; +} + +.chat-bubble-ai h2 { + font-size: 1.3em; + font-weight: bold; + margin: 0.5em 0; + color: #1f2937; +} + +.chat-bubble-ai h3 { + font-size: 1.1em; + font-weight: bold; + margin: 0.5em 0; + color: #1f2937; +} + +.chat-bubble-ai p { + margin: 0.5em 0; + line-height: 1.5; +} + +.chat-bubble-ai ul, .chat-bubble-ai ol { + margin: 0.5em 0; + padding-left: 1.5em; +} + +.chat-bubble-ai li { + margin: 0.25em 0; + line-height: 1.4; +} + +.chat-bubble-ai code { + background: #f3f4f6; + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 0.9em; + color: #e11d48; +} + +.chat-bubble-ai pre { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 0.75em; + overflow-x: auto; + margin: 0.5em 0; +} + +.chat-bubble-ai pre code { + background: none; + padding: 0; + color: #1f2937; +} + +.chat-bubble-ai blockquote { + border-left: 4px solid #d1d5db; + padding-left: 1em; + margin: 0.5em 0; + color: #6b7280; + font-style: italic; +} + +.chat-bubble-ai strong { + font-weight: bold; + color: #1f2937; +} + +.chat-bubble-ai em { + font-style: italic; + color: #4b5563; +} + +.chat-bubble-ai a { + color: #3b82f6; + text-decoration: underline; +} + +.chat-bubble-ai a:hover { + color: #1d4ed8; +} + +.chat-bubble-ai table { + border-collapse: collapse; + width: 100%; + margin: 0.5em 0; +} + +.chat-bubble-ai th, .chat-bubble-ai td { + border: 1px solid #e5e7eb; + padding: 0.5em; + text-align: left; +} + +.chat-bubble-ai th { + background: #f9fafb; + font-weight: bold; +} + /* SVG占位符样式 - 块级换行 + 新配色 */ .svg-placeholder-block { display: block; diff --git a/index.html b/index.html index 1859cda..22c6bcf 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ 产品画布 / SWOT分析 + diff --git a/js/app.js b/js/app.js index 4bd8e40..06f151a 100644 --- a/js/app.js +++ b/js/app.js @@ -2,6 +2,17 @@ * 应用核心逻辑 */ +// 配置Markdown解析器 +if (typeof marked !== 'undefined') { + marked.setOptions({ + breaks: true, // 支持换行 + gfm: true, // 支持GitHub风格的Markdown + sanitize: false, // 允许HTML(因为我们自己处理SVG) + smartLists: true, // 智能列表 + smartypants: true // 智能标点 + }); +} + class ProductCanvasApp { constructor() { this.currentMode = 'canvas'; // 'canvas' 或 'swot' @@ -217,29 +228,30 @@ class ProductCanvasApp { fullContent += content; // 检测SVG开始标记 - if (!svgStarted && (fullContent.includes('```svg') || fullContent.includes('``` -
- -

正在绘制${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}...

+ if (!svgStarted) { + // 使用正则表达式更准确地检测SVG代码块开始 + const svgStartMatch = fullContent.match(/```(?:svg)?\s*/i); + if (svgStartMatch) { + svgStarted = true; + svgId = Utils.generateId('svg'); + + // 提取SVG开始前的文本 + const svgStartIndex = svgStartMatch.index; + beforeText = fullContent.substring(0, svgStartIndex); + + // 显示绘制中占位符 + this.updateStreamingMessageWithPlaceholder(messageContainer, beforeText, svgId); + + // 初始化SVG显示区域 + this.svgViewer.innerHTML = ` +
+
+ +

正在绘制${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}...

+
-
- `; + `; + } } // 如果SVG已经开始,收集SVG内容 @@ -249,57 +261,57 @@ class ProductCanvasApp { const svgEndIndex = fullContent.indexOf('') + 6; // +6 是 '' 的长度 // 提取完整的SVG内容 - const svgStartIndex = Math.max( - fullContent.indexOf('```svg'), - fullContent.indexOf('``` ')) { - svgContent += ''; + const svgStartMatch = fullContent.match(/```(?:svg)?\s*/i); + if (svgStartMatch) { + const svgStartIndex = svgStartMatch.index; + let svgWithMarkers = fullContent.substring(svgStartIndex, svgEndIndex); + + // 移除代码块标记 + svgContent = svgWithMarkers.replace(/```(?:svg)?\s*/, '').replace(/```$/, '').trim(); + + // 补全SVG结束标签(如果没有的话) + if (!svgContent.endsWith('')) { + svgContent += ''; + } + + // 实时显示SVG + this.svgViewer.innerHTML = svgContent; + + // 存储SVG内容 + this.svgStorage[this.currentMode][svgId] = { + content: svgContent, + messageId: messageId, + mode: this.currentMode, + timestamp: new Date().toISOString() + }; + + // 更新占位符为可点击状态 + this.updatePlaceholderToClickable(messageContainer, svgId); + + // 重置SVG状态,继续接收剩余文本 + svgStarted = false; + const afterText = fullContent.substring(svgEndIndex); + this.updateStreamingMessageAfterSVG(messageContainer, beforeText, svgId, afterText); } - - // 实时显示SVG - this.svgViewer.innerHTML = svgContent; - - // 存储SVG内容 - this.svgStorage[this.currentMode][svgId] = { - content: svgContent, - messageId: messageId, - mode: this.currentMode, - timestamp: new Date().toISOString() - }; - - // 更新占位符为可点击状态 - this.updatePlaceholderToClickable(messageContainer, svgId); - - // 重置SVG状态,继续接收剩余文本 - svgStarted = false; - const afterText = fullContent.substring(svgEndIndex); - this.updateStreamingMessageAfterSVG(messageContainer, beforeText, svgId, afterText); - } else if (svgContent) { + } else { // SVG还在继续,更新内容 - const svgStartIndex = Math.max( - fullContent.indexOf('```svg'), - fullContent.indexOf('``` ')) { - tempSvgContent += ''; + const svgStartMatch = fullContent.match(/```(?:svg)?\s*/i); + if (svgStartMatch) { + const svgStartIndex = svgStartMatch.index; + let svgWithMarkers = fullContent.substring(svgStartIndex); + + // 移除代码块标记 + svgContent = svgWithMarkers.replace(/```(?:svg)?\s*/, '').replace(/```$/, '').trim(); + + // 补全SVG结束标签以便实时显示 + let tempSvgContent = svgContent; + if (!tempSvgContent.endsWith('')) { + tempSvgContent += ''; + } + + // 实时更新SVG显示 + this.svgViewer.innerHTML = tempSvgContent; } - - // 实时更新SVG显示 - this.svgViewer.innerHTML = tempSvgContent; } } else { // 普通文本更新 @@ -341,17 +353,25 @@ class ProductCanvasApp { updateStreamingMessage(container, content) { const contentDiv = container.querySelector('.typing-cursor'); if (contentDiv) { - contentDiv.textContent = content; + // 使用Markdown解析内容 + if (typeof marked !== 'undefined') { + contentDiv.innerHTML = marked.parse(content); + } else { + contentDiv.textContent = content; + } Utils.scrollToBottom(this.chatHistory); } } // 更新流式消息内容并显示SVG占位符 updateStreamingMessageWithPlaceholder(container, beforeText, svgId) { + // 使用Markdown解析beforeText + const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); + container.innerHTML = `
- ${Utils.escapeHtml(beforeText)} + ${parsedBeforeText}
🎨 正在绘制${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}...
@@ -375,14 +395,18 @@ class ProductCanvasApp { // 更新SVG后的消息内容 updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) { + // 使用Markdown解析文本 + const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); + const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); + container.innerHTML = `
- ${Utils.escapeHtml(beforeText)} + ${parsedBeforeText}
📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
-
${Utils.escapeHtml(afterText)}
+
${parsedAfterText}
`; @@ -412,14 +436,18 @@ class ProductCanvasApp { afterText = fullContent.substring(svgEndIndex); } + // 使用Markdown解析文本 + const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); + const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); + // 更新容器内容为包含SVG的消息 container.innerHTML = `
- ${Utils.escapeHtml(beforeText)} + ${parsedBeforeText}
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
- ${Utils.escapeHtml(afterText)} + ${parsedAfterText}
@@ -449,14 +477,18 @@ class ProductCanvasApp { this.viewSVG(newSvgId); + // 使用Markdown解析文本 + const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText); + const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText); + // 更新容器内容为包含SVG的消息 container.innerHTML = `
- ${Utils.escapeHtml(parsed.beforeText)} + ${parsedBeforeText}
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
- ${Utils.escapeHtml(parsed.afterText)} + ${parsedAfterText}
@@ -471,10 +503,13 @@ class ProductCanvasApp {
`; } else { + // 使用Markdown解析内容 + const parsedContent = typeof marked !== 'undefined' ? marked.parse(fullContent) : Utils.escapeHtml(fullContent); + // 更新容器内容为普通消息 container.innerHTML = `
- ${Utils.escapeHtml(fullContent)} + ${parsedContent}
@@ -629,7 +664,7 @@ class ProductCanvasApp { messageDiv.innerHTML = `
- ${Utils.escapeHtml(message.content)} + ${typeof marked !== 'undefined' ? marked.parse(message.content) : Utils.escapeHtml(message.content)}
@@ -656,11 +691,11 @@ class ProductCanvasApp { messageDiv.innerHTML = `
- ${Utils.escapeHtml(parsed.beforeText)} + ${typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText)}
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
- ${Utils.escapeHtml(parsed.afterText)} + ${typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText)}