feat(ui): optimize chat box height and add markdown rendering for agent messages
This commit is contained in:
parent
2f7dd95699
commit
4fd682ff77
13
package-lock.json
generated
13
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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'">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user