Brak opisu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ai-response.component.ts 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChanges } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { MatIcon } from '@angular/material/icon';
  4. import { MatCardModule } from '@angular/material/card';
  5. import { MatButtonModule } from '@angular/material/button';
  6. import { MatTooltipModule } from '@angular/material/tooltip';
  7. import { MatProgressBarModule } from '@angular/material/progress-bar';
  8. import { Subscription } from 'rxjs';
  9. import { EventService } from '../services/event.service';
  10. import { IndependentEventService } from '../services/independent-event.service';
  11. import { MarkdownPipe } from '../shared/pipes/markdown.pipe';
  12. export interface AIResponse {
  13. sessionId: string;
  14. content: string;
  15. timestamp: Date;
  16. type: 'thinking' | 'tool' | 'error' | 'message' | 'markdown';
  17. isComplete: boolean;
  18. }
  19. @Component({
  20. selector: 'app-ai-response',
  21. standalone: true,
  22. imports: [
  23. CommonModule,
  24. MatIcon,
  25. MatCardModule,
  26. MatButtonModule,
  27. MatTooltipModule,
  28. MatProgressBarModule,
  29. MarkdownPipe
  30. ],
  31. templateUrl: './ai-response.component.html',
  32. styleUrl: './ai-response.component.scss'
  33. })
  34. export class AIResponseComponent implements OnInit, OnDestroy, OnChanges {
  35. @Input() sessionId?: string;
  36. @Input() multipleSessionIds: string[] = [];
  37. @Input() independentEventService?: IndependentEventService;
  38. currentResponse: AIResponse | null = null;
  39. responses: AIResponse[] = [];
  40. isStreaming = false;
  41. streamingProgress = 0;
  42. showFullText = true;
  43. private subscriptions: Subscription = new Subscription();
  44. private typingInterval: any;
  45. private typingContent = '';
  46. private typingIndex = 0;
  47. constructor(private eventService: EventService) {}
  48. ngOnInit() {
  49. this.setupEventListeners();
  50. }
  51. ngOnChanges(changes: SimpleChanges) {
  52. if (changes['sessionId'] || changes['multipleSessionIds']) {
  53. this.resetResponse();
  54. this.setupEventListeners();
  55. }
  56. }
  57. ngOnDestroy() {
  58. this.subscriptions.unsubscribe();
  59. this.clearTypingInterval();
  60. }
  61. private setupEventListeners() {
  62. // 清除现有订阅
  63. this.subscriptions.unsubscribe();
  64. this.subscriptions = new Subscription();
  65. // 优先使用独立事件服务
  66. if (this.independentEventService) {
  67. console.log('🔍 [AIResponseComponent] 使用独立事件服务订阅事件');
  68. this.subscriptions.add(
  69. this.independentEventService.events$.subscribe(event => {
  70. // 独立服务已按会话过滤,直接处理
  71. const sessionId = this.sessionId || this.independentEventService?.getCurrentSessionId();
  72. if (sessionId) {
  73. this.processEvent(sessionId, event);
  74. }
  75. })
  76. );
  77. return;
  78. }
  79. // 没有独立服务,使用全局事件服务
  80. if (this.multipleSessionIds && this.multipleSessionIds.length > 0) {
  81. // 多会话模式 - 监听所有文档会话的事件
  82. this.multipleSessionIds.forEach(sessionId => {
  83. if (sessionId && sessionId.trim()) {
  84. this.subscriptions.add(
  85. this.eventService.subscribeToSessionEvents(sessionId).subscribe({
  86. next: (event) => {
  87. this.processEvent(sessionId, event);
  88. },
  89. error: (error) => console.error(`会话 ${sessionId} 事件监听错误:`, error)
  90. })
  91. );
  92. }
  93. });
  94. } else if (this.sessionId) {
  95. // 单会话模式 - 监听当前会话的事件
  96. this.subscriptions.add(
  97. this.eventService.subscribeToSessionEvents(this.sessionId!).subscribe({
  98. next: (event) => {
  99. this.processEvent(this.sessionId!, event);
  100. },
  101. error: (error) => console.error(`会话 ${this.sessionId!} 事件监听错误:`, error)
  102. })
  103. );
  104. }
  105. }
  106. private processEvent(sessionId: string, event: any) {
  107. console.log('🔍 [AIResponseComponent] 处理事件,sessionId:', sessionId, '事件类型:', event.payload?.type);
  108. console.log('🔍 [AIResponseComponent] 事件完整结构:', JSON.stringify(event, null, 2).substring(0, 300));
  109. const payload = event.payload;
  110. const timestamp = new Date();
  111. // 确定事件类型
  112. let type: AIResponse['type'] = 'message';
  113. if (payload.type === 'thinking' || payload.type === 'tool_call') {
  114. type = 'thinking';
  115. } else if (payload.type === 'tool' || payload.type === 'tool_result') {
  116. type = 'tool';
  117. } else if (payload.type === 'error') {
  118. type = 'error';
  119. } else if (payload.type === 'markdown' || payload.type?.includes('markdown')) {
  120. type = 'markdown';
  121. }
  122. // 提取内容
  123. let content = '';
  124. const properties = payload.properties || {};
  125. console.log('🔍 [AIResponseComponent] 事件属性:', properties);
  126. if (payload.type === 'message.updated') {
  127. // 处理消息更新事件
  128. const messageInfo = properties.info || {};
  129. console.log('🔍 [AIResponseComponent] message.updated消息信息:', messageInfo);
  130. if (messageInfo.content) {
  131. content = messageInfo.content;
  132. }
  133. } else if (properties.content) {
  134. content = properties.content;
  135. } else if (properties.message) {
  136. content = properties.message;
  137. } else if (properties.info && properties.info.content) {
  138. content = properties.info.content;
  139. } else {
  140. // 如果无法提取,将整个payload转换为字符串
  141. content = JSON.stringify(payload, null, 2);
  142. }
  143. // 如果是思考或工具调用,可能需要特殊处理
  144. if (type === 'thinking' || type === 'tool') {
  145. // 这些内容可以折叠显示
  146. content = content.replace(/^\[思考\]|^\[工具\]|^\[错误\]/, '').trim();
  147. }
  148. const newResponse: AIResponse = {
  149. sessionId,
  150. content,
  151. timestamp,
  152. type,
  153. isComplete: true
  154. };
  155. // 添加到响应列表
  156. this.responses.push(newResponse);
  157. // 如果是当前会话,开始打字机效果
  158. if ((this.multipleSessionIds && this.multipleSessionIds.includes(sessionId)) ||
  159. sessionId === this.sessionId) {
  160. this.startTypingEffect(newResponse);
  161. }
  162. // 限制响应数量,防止内存泄漏
  163. if (this.responses.length > 100) {
  164. this.responses = this.responses.slice(-50);
  165. }
  166. }
  167. private startTypingEffect(response: AIResponse) {
  168. this.clearTypingInterval();
  169. this.currentResponse = response;
  170. this.isStreaming = true;
  171. this.streamingProgress = 0;
  172. this.typingContent = response.content;
  173. this.typingIndex = 0;
  174. const totalChars = this.typingContent.length;
  175. if (totalChars === 0) {
  176. this.isStreaming = false;
  177. return;
  178. }
  179. // 计算打字速度(每秒50个字符)
  180. const speed = 50; // 字符/秒
  181. const interval = 1000 / speed;
  182. this.typingInterval = setInterval(() => {
  183. this.typingIndex++;
  184. this.streamingProgress = (this.typingIndex / totalChars) * 100;
  185. if (this.typingIndex >= totalChars) {
  186. this.isStreaming = false;
  187. this.clearTypingInterval();
  188. }
  189. }, interval);
  190. }
  191. private clearTypingInterval() {
  192. if (this.typingInterval) {
  193. clearInterval(this.typingInterval);
  194. this.typingInterval = null;
  195. }
  196. }
  197. getDisplayContent(): string {
  198. if (!this.currentResponse) return '';
  199. if (this.isStreaming) {
  200. return this.typingContent.substring(0, this.typingIndex);
  201. }
  202. return this.currentResponse.content;
  203. }
  204. getCurrentType(): AIResponse['type'] {
  205. return this.currentResponse?.type || 'message';
  206. }
  207. getCurrentSessionId(): string {
  208. return this.currentResponse?.sessionId || '';
  209. }
  210. getFormattedTimestamp(): string {
  211. if (!this.currentResponse?.timestamp) return '';
  212. const date = this.currentResponse.timestamp;
  213. return date.toLocaleTimeString('zh-CN', {
  214. hour: '2-digit',
  215. minute: '2-digit',
  216. second: '2-digit'
  217. });
  218. }
  219. toggleFullText() {
  220. this.showFullText = !this.showFullText;
  221. }
  222. clearResponses() {
  223. this.responses = [];
  224. this.currentResponse = null;
  225. this.isStreaming = false;
  226. this.streamingProgress = 0;
  227. }
  228. private resetResponse() {
  229. this.currentResponse = null;
  230. this.isStreaming = false;
  231. this.streamingProgress = 0;
  232. this.clearTypingInterval();
  233. }
  234. getTypeIcon(type: AIResponse['type']): string {
  235. switch (type) {
  236. case 'thinking': return 'psychology';
  237. case 'tool': return 'build';
  238. case 'error': return 'error';
  239. case 'markdown': return 'code';
  240. default: return 'chat';
  241. }
  242. }
  243. getTypeColor(type: AIResponse['type']): string {
  244. switch (type) {
  245. case 'thinking': return 'var(--thinking-color, #2196f3)';
  246. case 'tool': return 'var(--tool-color, #ff9800)';
  247. case 'error': return 'var(--error-color, #f44336)';
  248. case 'markdown': return 'var(--markdown-color, #673ab7)';
  249. default: return 'var(--message-color, #4caf50)';
  250. }
  251. }
  252. getTypeLabel(type: AIResponse['type']): string {
  253. switch (type) {
  254. case 'thinking': return '思考中';
  255. case 'tool': return '工具调用';
  256. case 'error': return '错误';
  257. case 'markdown': return '代码';
  258. default: return '消息';
  259. }
  260. }
  261. }