diff --git a/package-lock.json b/package-lock.json index cdbc9ad..17b63de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@angular/forms": "^20.3.0", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", + "marked": "^18.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -6931,6 +6932,18 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/marked": { + "version": "18.0.0", + "resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.0.tgz", + "integrity": "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index d7f88fd..b06873d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@angular/forms": "^20.3.0", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", + "marked": "^18.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -46,4 +47,4 @@ "ng-packagr": "^20.3.0", "typescript": "~5.9.2" } -} \ No newline at end of file +} diff --git a/src/app/app.css b/src/app/app.css index ed2d887..20f8dc4 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -393,3 +393,30 @@ background: var(--color-primary); color: white; border: none; width: 36px; height: 36px; border-radius: 50%; display: flex; justify-content: center; align-items: center; transition: all 0.2s; } .chat-send-btn:disabled { background: #cbd5e1; color: #f1f5f9; } +/* Markdown in Chat */ +.markdown-body { + font-family: inherit; + line-height: 1.6; +} +.markdown-body p { + margin-bottom: 8px; +} +.markdown-body p:last-child { + margin-bottom: 0; +} +.markdown-body strong { + font-weight: 700; +} +.markdown-body ul, .markdown-body ol { + padding-left: 20px; + margin-bottom: 8px; +} +.markdown-body li { + margin-bottom: 4px; +} +.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 { + margin-top: 12px; + margin-bottom: 8px; + font-weight: 700; + line-height: 1.4; +} diff --git a/src/app/app.html b/src/app/app.html index ba48b2d..68d29b1 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -122,7 +122,7 @@ -
+

💬 X光片追踪深打

🎙️ 上下文锁定:本次面诊录音报告

@@ -139,7 +139,7 @@
-
+
diff --git a/src/app/app.ts b/src/app/app.ts index a243820..07eb4df 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -8,6 +8,8 @@ import { ApiService } from './core/api.service'; import { AuthService } from './core/auth.service'; import { LoginComponent } from './pages/login/login'; import { ChatContextService } from './core/chat-context.service'; +import { marked } from 'marked'; + @Component({ selector: 'app-root', @@ -71,7 +73,7 @@ export class App implements OnDestroy, OnInit { chatPlaceholder = '向深维追问...'; showChatSheet = false; chatInput = ''; - chatMessages: { role: string, content: string, isStreaming?: boolean }[] = []; + chatMessages: { role: string, rawContent?: string, htmlContent?: string, isStreaming?: boolean }[] = []; agents = [ { id: 'asr', name: 'Qwen转写', status: 'pending', iconHtml: this.sanitizer.bypassSecurityTrustHtml(``) }, @@ -124,7 +126,8 @@ export class App implements OnDestroy, OnInit { case 'agent:chunk': const currentStreamingMsg = this.chatMessages.find(m => m.isStreaming && m.role === 'agent'); if (currentStreamingMsg) { - currentStreamingMsg.content += payload.text; + currentStreamingMsg.rawContent = (currentStreamingMsg.rawContent || '') + payload.text; + currentStreamingMsg.htmlContent = marked.parse(currentStreamingMsg.rawContent) as string; } break; case 'agent:thinking': @@ -136,14 +139,18 @@ export class App implements OnDestroy, OnInit { const activeStreamingMsg = this.chatMessages.find(m => m.isStreaming && m.role === 'agent'); if (activeStreamingMsg) { activeStreamingMsg.isStreaming = false; - activeStreamingMsg.content = payload.fullAnswer || activeStreamingMsg.content; + if (payload.fullAnswer) { + activeStreamingMsg.rawContent = payload.fullAnswer; + activeStreamingMsg.htmlContent = marked.parse(activeStreamingMsg.rawContent || '') as string; + } } break; case 'agent:error': const failedMsg = this.chatMessages.find(m => m.isStreaming && m.role === 'agent'); if (failedMsg) { failedMsg.isStreaming = false; - failedMsg.content = `[系统异常] ${payload.message}`; + failedMsg.htmlContent = `[系统异常] ${payload.message}`; + failedMsg.rawContent = failedMsg.htmlContent; } break; } @@ -449,11 +456,16 @@ export class App implements OnDestroy, OnInit { try { const res = await this.api.getChatHistory(this.chatCtx.chatId()); if (res && res.messages && res.messages.length > 0) { - this.chatMessages = res.messages.map((m: any) => ({ - role: m.role, - content: m.content, - isStreaming: false - })); + this.chatMessages = res.messages.map((m: any) => { + const rawContent = m.content || ''; + const htmlContent = marked.parse(rawContent) as string; + return { + role: m.role, + rawContent: rawContent, + htmlContent: htmlContent, + isStreaming: false + }; + }); } else { // No history, push initial greeting const title = this.chatCtx.contextTitle(); @@ -461,11 +473,13 @@ export class App implements OnDestroy, OnInit { const greeting = this.chatCtx.contextType() === 'client' ? `医生您好,上下文已锁定:${title}(全景档案)。您可以向我追问历史拒因分析或索要跨品类破冰话术。` : `医生您好,我是本场面诊的深维特勤 AI。上下文 (标识:${code}) 已锁定。您可以随时向我追问改进方案。`; - this.chatMessages.push({ role: 'agent', content: greeting, isStreaming: false }); + const parsed = marked.parse(greeting) as string; + this.chatMessages.push({ role: 'agent', rawContent: greeting, htmlContent: parsed, isStreaming: false }); } } catch(e) { console.error('Failed to load chat history:', e); - this.chatMessages.push({ role: 'agent', content: '医生您好,我是深维特勤 AI。无法获取历史记录,您可以直接向我提问。', isStreaming: false }); + const errStr = '医生您好,我是深维特勤 AI。无法获取历史记录,您可以直接向我提问。'; + this.chatMessages.push({ role: 'agent', rawContent: errStr, htmlContent: errStr, isStreaming: false }); } } } @@ -477,10 +491,11 @@ export class App implements OnDestroy, OnInit { this.chatInput = ''; // Push user message - this.chatMessages.push({ role: 'user', content: text, isStreaming: false }); + const parsedUser = marked.parse(text) as string; + this.chatMessages.push({ role: 'user', rawContent: text, htmlContent: parsedUser, isStreaming: false }); // Push agent placeholder - this.chatMessages.push({ role: 'agent', content: '', isStreaming: true }); + this.chatMessages.push({ role: 'agent', rawContent: '', htmlContent: '', isStreaming: true }); try { await this.api.sendChat(this.chatCtx.chatId(), text, this.chatCtx.contextId()); @@ -488,7 +503,8 @@ export class App implements OnDestroy, OnInit { const failMsg = this.chatMessages.find(m => m.isStreaming && m.role === 'agent'); if (failMsg) { failMsg.isStreaming = false; - failMsg.content = `发送失败: 请重试`; + failMsg.htmlContent = `发送失败: 请重试`; + failMsg.rawContent = failMsg.htmlContent; } } }