| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked, Input, OnChanges, SimpleChanges } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { MatCardModule } from '@angular/material/card';
- import { MatButtonModule } from '@angular/material/button';
- import { MatIconModule } from '@angular/material/icon';
- import { MatInputModule } from '@angular/material/input';
- import { MatFormFieldModule } from '@angular/material/form-field';
- import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
- import { FormsModule } from '@angular/forms';
- import { Subject, Subscription, of } from 'rxjs';
- import { distinctUntilChanged, switchMap, takeUntil, tap, catchError } from 'rxjs/operators';
- import { ConversationService } from '../services/conversation.service';
- import { SessionService } from '../services/session.service';
- import { EventService } from '../services/event.service';
- import { IndependentEventService } from '../services/independent-event.service';
- import { ChatMessage } from '../models/conversation.model';
- import { Session } from '../models/session.model';
- import { GlobalEvent, MessageUpdatedEvent, MessagePartUpdatedEvent, SessionUpdatedEvent } from '../models/event.model';
- import { MarkdownPipe } from '../shared/pipes/markdown.pipe';
-
- @Component({
- selector: 'app-conversation',
- standalone: true,
- imports: [
- CommonModule,
- MatCardModule,
- MatButtonModule,
- MatIconModule,
- MatInputModule,
- MatFormFieldModule,
- MatProgressSpinnerModule,
- FormsModule,
- MarkdownPipe
- ],
- templateUrl: './conversation.component.html',
- styleUrl: './conversation.component.scss',
- })
- export class ConversationComponent implements OnInit, OnDestroy, AfterViewChecked, OnChanges {
- @ViewChild('messagesContainer') private messagesContainer!: ElementRef;
- @ViewChild('messageInput') private messageInput!: ElementRef;
-
- @Input() instanceId: string | undefined;
- @Input() sessionId: string | undefined;
- @Input() independentEventService?: IndependentEventService;
-
- messages: ChatMessage[] = [];
- userInput = '';
- isLoading = false;
- activeSession: Session | null = null;
-
- private subscriptions: Subscription = new Subscription();
- private currentStreamSubscription: Subscription | null = null;
- private sessionEventSubscription: Subscription | null = null;
- private backendEventSubscription: Subscription | null = null;
- private shouldScroll = false;
- private isUserScrolling = false;
- private scrollDebounceTimeout: any = null;
- private isProgrammaticScroll = false; // 是否程序触发的滚动
- private isInThinking = false; // 是否正在思考过程中
- private thinkingStartIndex = 0; // 思考内容的起始位置(在消息内容中的索引)
-
- // RxJS Subjects for reactive session management
- private destroy$ = new Subject<void>();
- private sessionChanged$ = new Subject<string>();
- private currentLoadingSession: string | null = null;
- private isLoadingMessages = false;
-
- // RouteReuseStrategy生命周期方法
- private isDetached = false;
-
- constructor(
- private conversationService: ConversationService,
- private sessionService: SessionService,
- private eventService: EventService
- ) {}
-
- ngOnInit() {
- console.log('🔍 [ConversationComponent] 初始化,instanceId:', this.instanceId, 'sessionId:', this.sessionId, 'activeSession初始值:', this.activeSession?.id);
- console.log('🔍 [ConversationComponent] independentEventService:', this.independentEventService ? '已提供' : '未提供');
-
- // 如果提供了sessionId,直接使用该会话(独立模式)
- if (this.sessionId) {
- console.log('🔍 [ConversationComponent] 独立模式,直接加载会话:', this.sessionId);
- this.activeSession = { id: this.sessionId } as Session; // 临时会话对象
- console.log('🔍 [ConversationComponent] 设置activeSession:', this.activeSession.id);
- this.loadMessages(this.sessionId);
-
- // 设置后端事件订阅
- this.setupBackendEventSubscription();
-
- // 独立模式下不订阅全局活动会话变化
- console.log('🔍 [ConversationComponent] 独立模式,跳过全局活动会话订阅');
- } else {
- // 原有逻辑:依赖全局活动会话
- console.log('🔍 [ConversationComponent] 使用全局活动会话模式');
-
- // 立即检查当前是否有活动会话(处理组件在会话设置后初始化的情况)
- const currentSession = this.sessionService.getActiveSession();
- if (currentSession) {
- console.log('🔍 [ConversationComponent] 初始化时已有活动会话:', currentSession.id);
- this.activeSession = currentSession;
- this.loadMessages(currentSession.id);
-
- this.subscribeToSessionEvents(currentSession.id);
- }
-
- // 订阅活动会话变化 - 使用响应式管道避免重复加载
- this.subscriptions.add(
- this.sessionService.activeSession$.pipe(
- takeUntil(this.destroy$),
- // 避免相同会话重复触发
- distinctUntilChanged((prev, curr) => {
- const prevId = prev?.id;
- const currId = curr?.id;
- return prevId === currId;
- })
- ).subscribe(session => {
- console.log('🔍 [ConversationComponent] 活动会话变化:', session?.id);
- console.log('🔍 [ConversationComponent] 当前instanceId:', this.instanceId);
- console.log('🔍 [ConversationComponent] 当前sessionId输入:', this.sessionId);
-
- // 会话切换时取消当前正在进行的流式请求
- if (this.activeSession && this.activeSession.id !== session?.id) {
- console.log('🔍 [ConversationComponent] 会话切换,取消当前流式请求');
- this.cancelCurrentStream();
- }
-
- this.activeSession = session;
- console.log('🔍 [ConversationComponent] 设置activeSession为:', this.activeSession?.id || 'null');
- if (session) {
- console.log('🔍 [ConversationComponent] 加载会话消息:', session.id);
- this.loadMessages(session.id);
-
- // 订阅该会话的事件
- this.subscribeToSessionEvents(session.id);
- } else {
- console.log('🔍 [ConversationComponent] 无活动会话,清空消息');
- this.messages = [];
- // 取消会话事件订阅
- this.unsubscribeFromSessionEvents();
- }
- })
- );
- }
-
- // 设置后端事件订阅
- this.setupBackendEventSubscription();
-
- // 订阅新消息,过滤当前会话
- this.subscriptions.add(
- this.conversationService.newMessage$.subscribe(message => {
- if (message && message.sessionID) {
- // 检查消息是否属于当前会话
- const currentSessionId = this.sessionId || this.activeSession?.id;
- if (currentSessionId && message.sessionID === currentSessionId) {
- this.messages.push(message);
- this.shouldScroll = true;
- }
- }
- })
- );
-
- // 订阅流式更新,仅在当前组件处于加载状态时处理(避免跨会话干扰)
- this.subscriptions.add(
- this.conversationService.streamUpdate$.subscribe(update => {
- // 如果当前没有活动的流订阅,忽略(防止跨会话干扰)
- if (!this.currentStreamSubscription) return;
-
- console.log('🔍 [ConversationComponent] 收到流式更新:', update.type, 'data:', update.data?.substring(0, 50));
-
- // 找到最后一个AI消息
- let lastAIMessage = null;
- for (let i = this.messages.length - 1; i >= 0; i--) {
- if (this.messages[i].role === 'assistant') {
- lastAIMessage = this.messages[i];
- break;
- }
- }
-
- if (!lastAIMessage) return;
-
- switch (update.type) {
- case 'thinking':
- // 思考过程 - 只在开始时添加标签
- if (!this.isInThinking) {
- // 第一次思考,添加标签和换行
- lastAIMessage.content += '\n[思考] ' + update.data;
- this.isInThinking = true;
- this.thinkingStartIndex = lastAIMessage.content.length - update.data.length - 4; // 减去"[思考] "的长度
- } else {
- // 继续思考,直接追加内容
- lastAIMessage.content += update.data;
- }
- this.shouldScroll = true;
- break;
-
- case 'tool':
- // 工具调用 - 添加标签(每个工具调用独立)
- lastAIMessage.content += '\n[工具] ' + update.data;
- this.shouldScroll = true;
- break;
-
- case 'reply':
- // 最终回复 - 直接追加,清除思考状态
- this.isInThinking = false;
- lastAIMessage.content += update.data;
- this.shouldScroll = true;
- break;
-
- case 'error':
- // 错误信息 - 添加标签,清除思考状态
- this.isInThinking = false;
- console.log('🔍 [ConversationComponent] 错误,设置AI消息loading为false');
- lastAIMessage.content += '\n\n[错误] ' + update.data;
- lastAIMessage.loading = false;
- this.isLoading = false;
- this.focusInput();
- break;
-
- case 'done':
- // 完成标记 - 停止加载,清除思考状态
- this.isInThinking = false;
- console.log('🔍 [ConversationComponent] 设置AI消息loading为false');
- lastAIMessage.loading = false;
- this.isLoading = false;
- this.focusInput();
- break;
- }
- })
- );
- }
-
- ngOnChanges(changes: SimpleChanges) {
- console.log('🔍 [ConversationComponent] 输入属性变化:', changes);
- console.log('🔍 [ConversationComponent] 当前activeSession:', this.activeSession?.id);
-
- // 处理sessionId输入变化(独立模式)
- if (changes['sessionId'] && !changes['sessionId'].firstChange) {
- console.log('🔍 [ConversationComponent] sessionId变化,新值:', this.sessionId, '旧值:', changes['sessionId'].previousValue);
-
- // 取消当前流式请求
- this.cancelCurrentStream();
-
- // 取消会话事件订阅
- this.unsubscribeFromSessionEvents();
-
-
-
- if (this.sessionId) {
- // 独立模式:直接使用新的sessionId
- console.log('🔍 [ConversationComponent] 独立模式,重新加载会话消息:', this.sessionId);
- this.activeSession = { id: this.sessionId } as Session;
- console.log('🔍 [ConversationComponent] 设置activeSession为:', this.activeSession.id);
- this.loadMessages(this.sessionId);
-
-
-
- // 更新后端事件订阅
- this.setupBackendEventSubscription();
- } else {
- // 切换到全局模式
- console.log('🔍 [ConversationComponent] 切换到全局模式');
- this.activeSession = null;
- this.messages = [];
- console.log('🔍 [ConversationComponent] 设置activeSession为null');
- }
- }
-
- // 处理instanceId输入变化
- if (changes['instanceId'] && this.sessionId && this.instanceId) {
- // 更新后端事件订阅
- this.setupBackendEventSubscription();
- }
- }
-
- // 订阅后端推送的事件
- private setupBackendEventSubscription() {
- // 清理现有的后端事件订阅
- if (this.backendEventSubscription) {
- this.backendEventSubscription.unsubscribe();
- this.backendEventSubscription = null;
- }
-
- // 优先使用独立事件服务
- if (this.independentEventService) {
- console.log('🔍 [ConversationComponent] 使用独立事件服务订阅事件');
- this.backendEventSubscription = this.independentEventService.events$.subscribe(event => {
- this.handleBackendEvent(event);
- });
- } else {
- // 回退到全局事件服务
- console.log('🔍 [ConversationComponent] 使用全局事件服务订阅事件');
- this.backendEventSubscription = this.eventService.allEvents$.subscribe(event => {
- this.handleBackendEvent(event);
- });
- }
- }
-
- private unsubscribeBackendEvents() {
- if (this.backendEventSubscription) {
- this.backendEventSubscription.unsubscribe();
- this.backendEventSubscription = null;
- console.log('🔍 [ConversationComponent] 取消后端事件订阅');
- }
- }
-
- // 处理后端推送的事件
- private handleBackendEvent(event: GlobalEvent) {
- const payload = event.payload;
- console.log('🔍 [ConversationComponent] 收到后端事件:', payload.type);
-
- // 处理不同类型的后端事件
- switch (payload.type) {
- case 'message.updated':
- // 处理消息更新事件
- const messageEvent = payload as MessageUpdatedEvent;
- this.handleMessageUpdated(messageEvent);
- break;
- case 'message.part.updated':
- // 处理消息部分更新事件
- const partEvent = payload as MessagePartUpdatedEvent;
- this.handleMessagePartUpdated(partEvent);
- break;
- case 'session.updated':
- // 处理会话更新事件
- const sessionEvent = payload as SessionUpdatedEvent;
- this.handleSessionUpdated(sessionEvent);
- break;
- // 可以根据需要处理其他事件类型
- }
- }
-
- // 处理消息更新事件
- private handleMessageUpdated(event: MessageUpdatedEvent) {
- const messageInfo = event.properties.info;
- // 根据消息信息更新UI
- console.log('🔍 [ConversationComponent] 处理消息更新事件:', messageInfo.id, 'role:', messageInfo.role);
-
- // 找到对应的消息并更新
- const messageIndex = this.messages.findIndex(msg => msg.id === messageInfo.id);
- if (messageIndex !== -1) {
- // 更新现有消息
- if (messageInfo.content !== undefined) {
- this.messages[messageIndex].content = messageInfo.content;
- }
- this.shouldScroll = true;
- } else {
- // 创建新消息(处理从后端推送的新消息)
- const currentSessionId = this.sessionId || this.activeSession?.id;
- if (currentSessionId && messageInfo.role) {
- const newMessage: ChatMessage = {
- id: messageInfo.id,
- role: messageInfo.role as 'user' | 'assistant' | 'system',
- content: messageInfo.content || '',
- timestamp: new Date(),
- sessionID: currentSessionId,
- loading: false
- };
- this.messages.push(newMessage);
- this.shouldScroll = true;
- console.log('🔍 [ConversationComponent] 添加新消息:', newMessage.id, 'role:', newMessage.role);
- }
- }
- }
-
- // 处理消息部分更新事件
- private handleMessagePartUpdated(event: MessagePartUpdatedEvent) {
- const part = event.properties.part;
- const delta = event.properties.delta;
- console.log('🔍 [ConversationComponent] 处理消息部分更新事件:', part.type);
-
- // 这里需要根据具体业务逻辑处理消息部分更新
- // 例如,如果是文本部分更新,可以更新最后一条AI消息的内容
- if (part.type === 'text' && delta && this.messages.length > 0) {
- const lastMessage = this.messages[this.messages.length - 1];
- if (lastMessage.role === 'assistant') {
- lastMessage.content += delta;
- this.shouldScroll = true;
- }
- }
- }
-
- // 处理会话更新事件
- private handleSessionUpdated(event: SessionUpdatedEvent) {
- const sessionInfo = event.properties.info;
- console.log('🔍 [ConversationComponent] 处理会话更新事件:', sessionInfo.id);
-
- // 如果更新的是当前活动会话,更新会话标题等
- if (this.activeSession && this.activeSession.id === sessionInfo.id) {
- // 可以更新会话信息
- }
- }
-
- ngAfterViewChecked() {
- if (this.shouldScroll && !this.isUserScrolling) {
- this.scrollToBottom();
- this.shouldScroll = false;
- }
- }
-
- ngOnDestroy() {
- console.log('🔍 [ConversationComponent] 组件销毁');
- this.cleanupAllSubscriptions();
-
- // 清理消息加载订阅
- if (this.loadMessagesSubscription) {
- this.loadMessagesSubscription.unsubscribe();
- this.loadMessagesSubscription = null;
- }
- }
-
- // 取消当前的流式请求
- private cancelCurrentStream() {
- if (this.currentStreamSubscription) {
- console.log('🔍 [ConversationComponent] 取消当前的流式请求');
- this.currentStreamSubscription.unsubscribe();
- this.currentStreamSubscription = null;
- }
- // 重置加载状态
- if (this.isLoading) {
- this.isLoading = false;
- }
- // 重置所有AI消息的loading状态(如果存在)
- let resetCount = 0;
- for (let i = this.messages.length - 1; i >= 0; i--) {
- const message = this.messages[i];
- if (message.role === 'assistant' && message.loading) {
- message.loading = false;
- resetCount++;
- }
- }
- if (resetCount > 0) {
- console.log(`🔍 [ConversationComponent] 取消时重置${resetCount}个AI消息的loading状态`);
- }
- // 重置思考状态
- this.isInThinking = false;
- this.thinkingStartIndex = 0;
- }
-
- // 安全的计时器函数,避免重复计时器错误
- private safeTimeStart(label: string) {
- try {
- console.timeEnd(label);
- } catch(e) {
- // 忽略计时器不存在的错误
- }
- console.time(label);
- }
-
- private safeTimeEnd(label: string) {
- try {
- console.timeEnd(label);
- } catch(e) {
- // 忽略计时器不存在的错误
- }
- }
-
- private loadMessagesSubscription: Subscription | null = null;
-
- loadMessages(sessionId: string) {
- console.log('🔍 [ConversationComponent] loadMessages 被调用,sessionId:', sessionId);
- console.log('🔍 [ConversationComponent] 当前messages长度:', this.messages.length);
- console.log('🔍 [ConversationComponent] 当前activeSession:', this.activeSession?.id);
- console.log('🔍 [ConversationComponent] independentEventService:', this.independentEventService ? '已提供' : '未提供');
- console.log('🔍 [ConversationComponent] 缓存键:', this.sessionId || this.activeSession?.id);
-
- // 检查是否已有消息(组件复用场景)
- // 使用当前输入参数作为缓存键,避免路由复用导致的错误缓存
- const cacheKey = this.sessionId || this.activeSession?.id;
- if (this.messages.length > 0 && cacheKey === sessionId) {
- console.log('🔍 [ConversationComponent] 已有消息,会话未变化,跳过重新加载');
- this.shouldScroll = true;
- // 独立实例页面:滚动到底部并聚焦输入框
- if (this.sessionId) { // 独立模式
- setTimeout(() => {
- this.scrollToBottom();
- this.focusInput();
- }, 100);
- }
- return;
- }
-
- // 取消之前的加载请求
- if (this.loadMessagesSubscription) {
- console.log('🔍 [ConversationComponent] 取消之前的消息加载请求');
- this.loadMessagesSubscription.unsubscribe();
- this.loadMessagesSubscription = null;
- }
-
- // 安全地开始计时器
- this.safeTimeStart(`[对话消息-加载] ${sessionId}`);
- console.log('📊 [对话消息] 开始加载会话消息,sessionId:', sessionId);
-
- this.messages = [];
-
- // 从服务加载历史消息
- this.loadMessagesSubscription = this.conversationService.getHistory(sessionId, 50).subscribe({
- next: (historyMessages) => {
- this.safeTimeEnd(`[对话消息-加载] ${sessionId}`);
- console.log('📊 [对话消息] 收到历史消息数量:', historyMessages.length);
-
- // 开始消息处理和渲染计时
- this.safeTimeStart(`[对话消息-处理] ${sessionId}`);
-
- this.messages = historyMessages;
-
- // 按时间排序(最早的在前)
- this.messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
-
- this.shouldScroll = true;
-
- // 独立实例页面:加载历史后滚动到底部并聚焦输入框
- if (this.sessionId) { // 独立模式
- setTimeout(() => {
- this.scrollToBottom();
- this.focusInput();
- console.log('🔍 [ConversationComponent] 独立实例页面:历史加载完成,滚动到底部并聚焦输入框');
- }, 100);
- }
-
- // 使用setTimeout估算渲染完成时间
- setTimeout(() => {
- this.safeTimeEnd(`[对话消息-处理] ${sessionId}`);
- console.log('📊 [对话消息] 消息处理和渲染完成,会话:', sessionId);
- }, 0);
-
- // 清理订阅引用
- this.loadMessagesSubscription = null;
- },
- error: (error) => {
- this.safeTimeEnd(`[对话消息-加载] ${sessionId}`);
- console.error('📊 [对话消息] 加载历史消息失败:', error);
- // 历史消息加载失败,但页面仍然可以正常工作
- // 用户仍然可以发送新消息
- this.loadMessagesSubscription = null;
- }
- });
- }
-
- // 订阅会话事件
- private subscribeToSessionEvents(sessionId: string) {
- // 取消现有订阅
- this.unsubscribeFromSessionEvents();
-
- if (!sessionId) return;
-
- // 如果有独立事件服务,则跳过会话事件订阅(独立服务已处理)
- if (this.independentEventService) {
- console.log(`🔍 [ConversationComponent] 使用独立事件服务,跳过单独会话订阅`);
- return;
- }
-
- // 使用全局事件服务订阅该会话的事件
- this.sessionEventSubscription = this.eventService.subscribeToSessionEvents(sessionId)
- .subscribe(event => {
- console.log(`收到会话 ${sessionId} 的事件:`, event.payload.type);
-
- // 处理消息更新事件
- if (event.payload.type === 'message.updated') {
- const messageEvent = event.payload as any;
- const messageInfo = messageEvent.properties?.info;
- if (messageInfo) {
- // 处理新消息
- const newMessage: ChatMessage = {
- id: messageInfo.id || Date.now().toString(),
- role: messageInfo.role || 'assistant',
- content: messageInfo.content || '',
- timestamp: new Date(),
- sessionID: sessionId, // 添加sessionID字段
- loading: false
- };
-
- // 添加到消息列表
- this.messages.push(newMessage);
- this.shouldScroll = true;
- }
- }
- // 可以添加其他事件类型的处理
- });
-
- console.log(`已订阅会话 ${sessionId} 的事件`);
- }
-
- // 取消会话事件订阅
- private unsubscribeFromSessionEvents() {
- if (this.sessionEventSubscription) {
- this.sessionEventSubscription.unsubscribe();
- this.sessionEventSubscription = null;
- console.log('已取消会话事件订阅');
- }
- }
-
- onSendMessage(event: any) {
- if (event.key === 'Enter' && !event.shiftKey) {
- event.preventDefault();
- this.sendMessage();
- }
- }
-
- sendMessage() {
- if (!this.userInput.trim() || !this.activeSession) return;
-
-
-
- // 取消之前可能仍在进行的流式请求(允许打断之前的回答)
- this.cancelCurrentStream();
-
- const userMessage: ChatMessage = {
- id: Date.now().toString(),
- role: 'user',
- content: this.userInput,
- timestamp: new Date(),
- sessionID: this.activeSession.id
- };
-
- // 添加用户消息
- this.messages.push(userMessage);
- this.shouldScroll = true;
- // 用户发送新消息时重置手动滚动标志
- this.isUserScrolling = false;
- if (this.scrollDebounceTimeout) {
- clearTimeout(this.scrollDebounceTimeout);
- this.scrollDebounceTimeout = null;
- }
-
- // 添加AI响应占位
- const aiMessage: ChatMessage = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: '',
- timestamp: new Date(),
- sessionID: this.activeSession.id,
- loading: true
- };
- console.log('🔍 [ConversationComponent] 创建AI消息,loading=true');
- this.messages.push(aiMessage);
- this.shouldScroll = true;
-
- // 重置思考状态
- this.isInThinking = false;
- this.thinkingStartIndex = 0;
-
- // 发送到服务
- this.isLoading = true;
- this.currentStreamSubscription = this.conversationService.sendMessage(
- this.activeSession.id,
- this.userInput
- ).subscribe({
- next: () => {
- // 流式连接已建立,但传输仍在继续
- // 消息已成功发送,立即恢复发送按钮状态,允许连续发送
- this.isLoading = false;
- },
- error: (error) => {
- console.error('发送消息失败:', error);
- aiMessage.content = '抱歉,发送消息时出现错误: ' + error.message;
- aiMessage.loading = false;
- this.isLoading = false;
- this.currentStreamSubscription = null;
- this.focusInput();
- },
- complete: () => {
- // 流式完成已在streamUpdate$中处理
- this.currentStreamSubscription = null;
- }
- });
-
- // 清空输入框
- this.userInput = '';
-
- // 聚焦输入框
- this.focusInput();
- }
-
-
-
- onMessagesContainerScroll() {
- // 如果是程序滚动,跳过用户滚动检测
- if (this.isProgrammaticScroll) {
- this.isProgrammaticScroll = false;
- return;
- }
-
- // 用户手动滚动时触发
- this.isUserScrolling = true;
-
- // 清除之前的超时
- if (this.scrollDebounceTimeout) {
- clearTimeout(this.scrollDebounceTimeout);
- }
-
- // 设置超时,2秒后重置滚动标志
- this.scrollDebounceTimeout = setTimeout(() => {
- this.isUserScrolling = false;
- }, 2000);
- }
-
- private scrollToBottom(retryCount = 0) {
- try {
- this.isProgrammaticScroll = true;
- const container = this.messagesContainer.nativeElement;
-
- // 强制回流,确保scrollHeight包含完整布局
- container.offsetHeight;
-
- const targetScrollTop = container.scrollHeight;
- container.scrollTop = targetScrollTop;
-
- // 如有误差,下一帧立即重试(16ms内完成)
- if (retryCount < 2 && Math.abs(container.scrollTop - targetScrollTop) > 1) {
- requestAnimationFrame(() => this.scrollToBottom(retryCount + 1));
- }
- } catch (err) {
- console.error('滚动失败:', err);
- }
- }
-
- private focusInput() {
- // 延迟聚焦以确保DOM已更新
- setTimeout(() => {
- if (this.messageInput?.nativeElement) {
- this.messageInput.nativeElement.focus();
- }
- }, 100);
- }
-
- /**
- * RouteReuseStrategy生命周期方法:组件被分离时调用
- * 组件分离期间,SSE订阅继续接收事件,但变更检测暂停
- */
- pauseEventProcessing(): void {
- this.isDetached = true;
- console.log('🔍 [ConversationComponent] 组件分离,暂停变更检测,SSE订阅保持');
- }
-
- /**
- * RouteReuseStrategy生命周期方法:组件被重新附加时调用
- */
- resumeEventProcessing(): void {
- this.isDetached = false;
- console.log('🔍 [ConversationComponent] 组件重新附加,恢复变更检测');
- // 触发变更检测以确保UI更新
- this.shouldScroll = true;
- }
-
- /**
- * RouteReuseStrategy生命周期方法:组件缓存被清理前调用
- * 这里取消所有订阅,因为组件即将被销毁
- */
- cleanupBeforeCacheRemoval(): void {
- console.log('🔍 [ConversationComponent] 清理缓存前的订阅');
- this.cleanupAllSubscriptions();
- }
-
- /**
- * 清理所有订阅(仅在组件真正销毁或缓存清理时调用)
- */
- private cleanupAllSubscriptions(): void {
- // 取消所有RxJS订阅
- this.subscriptions.unsubscribe();
- this.subscriptions = new Subscription();
-
- // 取消当前流式请求
- this.cancelCurrentStream();
-
- // 取消会话事件订阅
- this.unsubscribeFromSessionEvents();
-
- // 取消后端事件订阅
- this.unsubscribeBackendEvents();
-
-
-
- // 清理RxJS Subjects
- this.destroy$.next();
- this.destroy$.complete();
- this.sessionChanged$.complete();
- }
- }
|