feat(ui): optimize chat box height and add markdown rendering for agent messages

This commit is contained in:
lidf 2026-04-12 16:11:39 +08:00
parent 2f7dd95699
commit 4fd682ff77
5 changed files with 74 additions and 17 deletions

13
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -122,7 +122,7 @@
</ng-container>
<!-- STATE 3: Chat Sheet -->
<div class="chat-state" *ngIf="showChatSheet" style="display:flex; flex-direction:column; height: 50vh;">
<div class="chat-state" *ngIf="showChatSheet" style="display:flex; flex-direction:column; height: 85vh;">
<div class="chat-header">
<h4>💬 X光片追踪深打</h4>
<p *ngIf="chatCtx.contextType() === 'recording'">🎙️ 上下文锁定:本次面诊录音报告</p>
@ -139,7 +139,7 @@
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</div>
<div class="msg-bubble" [class.streaming]="msg.isStreaming" [innerHTML]="msg.content"></div>
<div class="msg-bubble markdown-body" [class.streaming]="msg.isStreaming" [innerHTML]="msg.htmlContent || msg.rawContent"></div>
<!-- 用户头像:圆形 -->
<div class="msg-avatar user-avatar" *ngIf="msg.role === 'user'">

View File

@ -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(`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 7 4 4 20 4 20 7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line></svg>`) },
@ -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) => ({
this.chatMessages = res.messages.map((m: any) => {
const rawContent = m.content || '';
const htmlContent = marked.parse(rawContent) as string;
return {
role: m.role,
content: m.content,
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;
}
}
}