import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatIcon } from '@angular/material/icon'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { Subscription } from 'rxjs'; import { EventService } from '../services/event.service'; import { IndependentEventService } from '../services/independent-event.service'; import { MarkdownPipe } from '../shared/pipes/markdown.pipe'; export interface AIResponse { sessionId: string; content: string; timestamp: Date; type: 'thinking' | 'tool' | 'error' | 'message' | 'markdown'; isComplete: boolean; } @Component({ selector: 'app-ai-response', standalone: true, imports: [ CommonModule, MatIcon, MatCardModule, MatButtonModule, MatTooltipModule, MatProgressBarModule, MarkdownPipe ], templateUrl: './ai-response.component.html', styleUrl: './ai-response.component.scss' }) export class AIResponseComponent implements OnInit, OnDestroy, OnChanges { @Input() sessionId?: string; @Input() multipleSessionIds: string[] = []; @Input() independentEventService?: IndependentEventService; currentResponse: AIResponse | null = null; responses: AIResponse[] = []; isStreaming = false; streamingProgress = 0; showFullText = true; private subscriptions: Subscription = new Subscription(); private typingInterval: any; private typingContent = ''; private typingIndex = 0; constructor(private eventService: EventService) {} ngOnInit() { this.setupEventListeners(); } ngOnChanges(changes: SimpleChanges) { if (changes['sessionId'] || changes['multipleSessionIds']) { this.resetResponse(); this.setupEventListeners(); } } ngOnDestroy() { this.subscriptions.unsubscribe(); this.clearTypingInterval(); } private setupEventListeners() { // 清除现有订阅 this.subscriptions.unsubscribe(); this.subscriptions = new Subscription(); // 优先使用独立事件服务 if (this.independentEventService) { console.log('🔍 [AIResponseComponent] 使用独立事件服务订阅事件'); this.subscriptions.add( this.independentEventService.events$.subscribe(event => { // 独立服务已按会话过滤,直接处理 const sessionId = this.sessionId || this.independentEventService?.getCurrentSessionId(); if (sessionId) { this.processEvent(sessionId, event); } }) ); return; } // 没有独立服务,使用全局事件服务 if (this.multipleSessionIds && this.multipleSessionIds.length > 0) { // 多会话模式 - 监听所有文档会话的事件 this.multipleSessionIds.forEach(sessionId => { if (sessionId && sessionId.trim()) { this.subscriptions.add( this.eventService.subscribeToSessionEvents(sessionId).subscribe({ next: (event) => { this.processEvent(sessionId, event); }, error: (error) => console.error(`会话 ${sessionId} 事件监听错误:`, error) }) ); } }); } else if (this.sessionId) { // 单会话模式 - 监听当前会话的事件 this.subscriptions.add( this.eventService.subscribeToSessionEvents(this.sessionId!).subscribe({ next: (event) => { this.processEvent(this.sessionId!, event); }, error: (error) => console.error(`会话 ${this.sessionId!} 事件监听错误:`, error) }) ); } } private processEvent(sessionId: string, event: any) { console.log('🔍 [AIResponseComponent] 处理事件,sessionId:', sessionId, '事件类型:', event.payload?.type); console.log('🔍 [AIResponseComponent] 事件完整结构:', JSON.stringify(event, null, 2).substring(0, 300)); const payload = event.payload; const timestamp = new Date(); // 确定事件类型 let type: AIResponse['type'] = 'message'; if (payload.type === 'thinking' || payload.type === 'tool_call') { type = 'thinking'; } else if (payload.type === 'tool' || payload.type === 'tool_result') { type = 'tool'; } else if (payload.type === 'error') { type = 'error'; } else if (payload.type === 'markdown' || payload.type?.includes('markdown')) { type = 'markdown'; } // 提取内容 let content = ''; const properties = payload.properties || {}; console.log('🔍 [AIResponseComponent] 事件属性:', properties); if (payload.type === 'message.updated') { // 处理消息更新事件 const messageInfo = properties.info || {}; console.log('🔍 [AIResponseComponent] message.updated消息信息:', messageInfo); if (messageInfo.content) { content = messageInfo.content; } } else if (properties.content) { content = properties.content; } else if (properties.message) { content = properties.message; } else if (properties.info && properties.info.content) { content = properties.info.content; } else { // 如果无法提取,将整个payload转换为字符串 content = JSON.stringify(payload, null, 2); } // 如果是思考或工具调用,可能需要特殊处理 if (type === 'thinking' || type === 'tool') { // 这些内容可以折叠显示 content = content.replace(/^\[思考\]|^\[工具\]|^\[错误\]/, '').trim(); } const newResponse: AIResponse = { sessionId, content, timestamp, type, isComplete: true }; // 添加到响应列表 this.responses.push(newResponse); // 如果是当前会话,开始打字机效果 if ((this.multipleSessionIds && this.multipleSessionIds.includes(sessionId)) || sessionId === this.sessionId) { this.startTypingEffect(newResponse); } // 限制响应数量,防止内存泄漏 if (this.responses.length > 100) { this.responses = this.responses.slice(-50); } } private startTypingEffect(response: AIResponse) { this.clearTypingInterval(); this.currentResponse = response; this.isStreaming = true; this.streamingProgress = 0; this.typingContent = response.content; this.typingIndex = 0; const totalChars = this.typingContent.length; if (totalChars === 0) { this.isStreaming = false; return; } // 计算打字速度(每秒50个字符) const speed = 50; // 字符/秒 const interval = 1000 / speed; this.typingInterval = setInterval(() => { this.typingIndex++; this.streamingProgress = (this.typingIndex / totalChars) * 100; if (this.typingIndex >= totalChars) { this.isStreaming = false; this.clearTypingInterval(); } }, interval); } private clearTypingInterval() { if (this.typingInterval) { clearInterval(this.typingInterval); this.typingInterval = null; } } getDisplayContent(): string { if (!this.currentResponse) return ''; if (this.isStreaming) { return this.typingContent.substring(0, this.typingIndex); } return this.currentResponse.content; } getCurrentType(): AIResponse['type'] { return this.currentResponse?.type || 'message'; } getCurrentSessionId(): string { return this.currentResponse?.sessionId || ''; } getFormattedTimestamp(): string { if (!this.currentResponse?.timestamp) return ''; const date = this.currentResponse.timestamp; return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } toggleFullText() { this.showFullText = !this.showFullText; } clearResponses() { this.responses = []; this.currentResponse = null; this.isStreaming = false; this.streamingProgress = 0; } private resetResponse() { this.currentResponse = null; this.isStreaming = false; this.streamingProgress = 0; this.clearTypingInterval(); } getTypeIcon(type: AIResponse['type']): string { switch (type) { case 'thinking': return 'psychology'; case 'tool': return 'build'; case 'error': return 'error'; case 'markdown': return 'code'; default: return 'chat'; } } getTypeColor(type: AIResponse['type']): string { switch (type) { case 'thinking': return 'var(--thinking-color, #2196f3)'; case 'tool': return 'var(--tool-color, #ff9800)'; case 'error': return 'var(--error-color, #f44336)'; case 'markdown': return 'var(--markdown-color, #673ab7)'; default: return 'var(--message-color, #4caf50)'; } } getTypeLabel(type: AIResponse['type']): string { switch (type) { case 'thinking': return '思考中'; case 'tool': return '工具调用'; case 'error': return '错误'; case 'markdown': return '代码'; default: return '消息'; } } }