| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- 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 '消息';
- }
- }
- }
|