| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- import { Component, OnInit, OnDestroy, HostListener, Input, OnChanges, SimpleChanges } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { ActivatedRoute } from '@angular/router';
- import { MatIcon } from '@angular/material/icon';
- import { MatButtonModule } from '@angular/material/button';
- import { MatCardModule } from '@angular/material/card';
- import { MatListModule } from '@angular/material/list';
- import { MatExpansionModule } from '@angular/material/expansion';
- import { MatChipsModule } from '@angular/material/chips';
- import { MatProgressBarModule } from '@angular/material/progress-bar';
- import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
- import { MatTooltipModule } from '@angular/material/tooltip';
- import { Subscription } from 'rxjs';
- import { ConversationComponent } from './conversation.component';
- import { DocumentListComponent } from './document-list.component';
- import { AIResponseComponent } from './ai-response.component';
-
- import { SessionService } from '../services/session.service';
- import { AgentService } from '../services/agent.service';
- import { MockDataService } from '../services/mock-data.service';
- import { EventService } from '../services/event.service';
- import { IndependentEventService } from '../services/independent-event.service';
- import { Session, SessionStatus } from '../models/session.model';
- import { DocumentType, DocumentSession } from '../models/document.model';
-
- @Component({
- selector: 'app-project-tab',
- standalone: true,
- imports: [
- CommonModule,
- MatIcon,
- MatButtonModule,
- MatCardModule,
- MatListModule,
- MatExpansionModule,
- MatChipsModule,
- MatProgressBarModule,
- MatProgressSpinnerModule,
- MatTooltipModule,
- ConversationComponent,
- DocumentListComponent,
- AIResponseComponent
- ],
- templateUrl: './project-tab.component.html',
- styleUrl: './project-tab.component.scss',
- })
- export class ProjectTabComponent implements OnInit, OnDestroy, OnChanges {
- @Input() tabId?: string;
- @Input() standalone = false;
- @Input() instanceId?: string;
- @Input() projectId?: string;
- @Input() independentEventService?: IndependentEventService;
-
- project: Session | null = null;
-
- effectiveInstanceId: string = '';
-
- // 文档数据
- documentSessions: DocumentSession[] = [];
-
- leftWidth = 320;
- middleWidth = 500;
- isDragging = false;
- isDraggingMiddle = false;
- minLeftWidth = 280;
- maxLeftWidth = 500;
- minMiddleWidth = 400;
- maxMiddleWidth = 800;
-
- private subscriptions: Subscription = new Subscription();
- private eventSubscription?: Subscription;
-
- constructor(
-
- private sessionService: SessionService,
- private agentService: AgentService,
- private mockDataService: MockDataService,
- private eventService: EventService,
- private route: ActivatedRoute
- ) {}
-
- ngOnInit() {
- // 生成或使用传入的实例ID
- this.updateEffectiveInstanceId();
-
- // 从本地存储恢复宽度
- const savedLeftWidth = localStorage.getItem('project_tab_leftWidth');
- if (savedLeftWidth) {
- this.leftWidth = parseInt(savedLeftWidth, 10);
- }
- const savedMiddleWidth = localStorage.getItem('project_tab_middleWidth');
- if (savedMiddleWidth) {
- this.middleWidth = parseInt(savedMiddleWidth, 10);
- }
-
- // 监听路由参数变化或使用输入参数
- if (this.projectId) {
- // 使用输入的项目ID
- this.loadProjectDirectly(this.projectId);
- } else {
- // 监听路由参数
- this.subscriptions.add(
- this.route.paramMap.subscribe(params => {
- const projectId = params.get('projectId');
- if (projectId) {
- this.loadProjectDirectly(projectId);
- }
- })
- );
- }
-
-
-
-
-
-
- }
-
- ngOnChanges(changes: SimpleChanges) {
- console.log('🔍 [ProjectTabComponent] 输入属性变化:', changes);
-
- // 处理instanceId变化
- if (changes['instanceId']) {
- console.log('🔍 [ProjectTabComponent] instanceId变化,更新有效实例ID');
- this.updateEffectiveInstanceId();
- }
-
- if (changes['projectId'] && !changes['projectId'].firstChange) {
- console.log('🔍 [ProjectTabComponent] 项目ID变化,重新加载项目:', this.projectId);
-
- // 清理旧的订阅
- this.subscriptions.unsubscribe();
- this.subscriptions = new Subscription();
-
- // 取消文档事件订阅
- if (this.eventSubscription) {
- this.eventSubscription.unsubscribe();
- this.eventSubscription = undefined;
- }
-
- // 重置项目数据
- this.project = null;
- this.documentSessions = [];
-
- // 项目变化时更新实例ID,确保SSE事件路由正确
- this.updateEffectiveInstanceId();
-
- // 重新加载项目
- if (this.projectId) {
- this.loadProjectDirectly(this.projectId);
- }
- }
- }
-
-
-
- private loadProjectDirectly(projectId: string) {
- console.log('🔍 [ProjectTabComponent] 独立模式加载项目:', projectId);
- // 直接从会话服务加载项目,不依赖标签页
- this.subscriptions.add(
- this.sessionService.sessions$.subscribe(sessions => {
- console.log('🔍 [ProjectTabComponent] 收到会话列表,数量:', sessions.length, '查找项目ID:', projectId);
-
- // 首先尝试用project_id匹配(项目ID)
- let session = sessions.find(s => s.project_id === projectId);
- if (session) {
- console.log('🔍 [ProjectTabComponent] 通过project_id找到会话:', session.id, '标题:', session.title);
- } else {
- // 如果没找到,尝试用id匹配(可能是会话ID)
- session = sessions.find(s => s.id === projectId);
- if (session) {
- console.log('🔍 [ProjectTabComponent] 通过id找到会话:', session.id, '标题:', session.title, 'project_id:', session.project_id);
- }
- }
-
- if (session && session !== this.project) {
- console.log('🔍 [ProjectTabComponent] 设置项目会话:', session.id);
- this.project = session;
- this.updateEffectiveInstanceId();
- // 独立模式下不设置全局活动会话,避免状态污染
- // this.sessionService.setActiveSession(session);
- this.loadDocumentSessions();
- } else if (!session) {
- console.log('🔍 [ProjectTabComponent] 未找到对应会话,等待会话加载');
- }
- })
- );
- // 初始加载会话列表
- this.sessionService.loadSessions();
- }
-
- getAgentDisplayName(agentId: string): string {
- return this.agentService.getAgentDisplayName(agentId);
- }
-
- getStatusDisplayName(status: string): string {
- const statusMap: Record<string, string> = {
- 'requirement_document': '需求文档',
- 'technical_document': '技术文档',
- 'code': '代码开发',
- 'test': '测试',
- 'release': '发布'
- };
- return statusMap[status] || status;
- }
-
- getStatusClass(status: string): string {
- return `status-${status}`;
- }
-
- formatDate(dateString?: string): string {
- if (!dateString) return '未知';
- const date = new Date(dateString);
- return date.toLocaleDateString('zh-CN');
- }
-
- getProgressValue(status: string): number {
- const progressMap: Record<string, number> = {
- 'requirement_document': 20,
- 'technical_document': 40,
- 'code': 60,
- 'test': 80,
- 'release': 100
- };
- return progressMap[status] || 0;
- }
-
- getProgressText(status: string): string {
- const progressMap: Record<string, string> = {
- 'requirement_document': '20% - 需求分析',
- 'technical_document': '40% - 技术设计',
- 'code': '60% - 代码开发',
- 'test': '80% - 测试验证',
- 'release': '100% - 发布完成'
- };
- return progressMap[status] || '0% - 未开始';
- }
-
- focusRequirementInput() {
- // TODO: 聚焦到对话输入框,并提示输入需求
- console.log('聚焦需求输入');
- // 可以通过服务或事件通知ConversationComponent
- }
-
- startDrag(event: MouseEvent) {
- event.preventDefault();
- this.isDragging = true;
- document.body.style.cursor = 'col-resize';
- document.body.style.userSelect = 'none';
- }
-
- startMiddleDrag(event: MouseEvent) {
- event.preventDefault();
- this.isDraggingMiddle = true;
- document.body.style.cursor = 'col-resize';
- document.body.style.userSelect = 'none';
- }
-
- @HostListener('document:mousemove', ['$event'])
- onDrag(event: MouseEvent) {
- if (this.isDragging) {
- const newWidth = event.clientX;
- if (newWidth >= this.minLeftWidth && newWidth <= this.maxLeftWidth) {
- this.leftWidth = newWidth;
- }
- }
- if (this.isDraggingMiddle) {
- const newWidth = event.clientX - this.leftWidth - 8;
- if (newWidth >= this.minMiddleWidth && newWidth <= this.maxMiddleWidth) {
- this.middleWidth = newWidth;
- }
- }
- }
-
- @HostListener('document:mouseup')
- stopDrag() {
- if (this.isDragging) {
- this.isDragging = false;
- localStorage.setItem('project_tab_leftWidth', this.leftWidth.toString());
- }
- if (this.isDraggingMiddle) {
- this.isDraggingMiddle = false;
- localStorage.setItem('project_tab_middleWidth', this.middleWidth.toString());
- }
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- }
-
- /**
- * 加载文档会话数据
- */
- private loadDocumentSessions() {
- if (!this.project) return;
-
- const projectId = this.project.project_id || this.project.id;
- this.mockDataService.getProjectDocuments(projectId).subscribe({
- next: (documents) => {
- this.documentSessions = documents;
-
- // 设置文档会话的事件订阅
- this.setupDocumentEventSubscriptions();
- },
- error: (error) => {
- console.error('加载文档数据失败:', error);
- // 使用空文档作为回退
- import('../models/document.model').then(module => {
- this.documentSessions = module.createEmptyDocumentSessions(projectId);
- // 即使使用空文档,也尝试设置事件订阅
- this.setupDocumentEventSubscriptions();
- });
- }
- });
- }
-
- /**
- * 设置文档会话的事件订阅
- */
- private setupDocumentEventSubscriptions() {
- // 取消现有的事件订阅
- if (this.eventSubscription) {
- this.eventSubscription.unsubscribe();
- this.eventSubscription = undefined;
- }
-
- // 获取有效的文档会话ID
- const sessionIds = this.getDocumentSessionIds();
- if (sessionIds.length === 0) {
- console.log('没有有效的文档会话ID可订阅');
- return;
- }
-
- console.log(`设置文档事件订阅,会话ID:`, sessionIds);
-
- // 连接到多会话事件流
- this.eventService.connectToMultipleSessions(sessionIds);
-
- // 订阅多会话事件
- this.eventSubscription = this.eventService.multiSessionEvents$.subscribe({
- next: ({ sessionId, event }) => {
- this.handleDocumentEvent(sessionId, event);
- },
- error: (error) => {
- console.error('文档事件订阅错误:', error);
- }
- });
- }
-
- /**
- * 处理文档事件,更新文档内容
- */
- private handleDocumentEvent(sessionId: string, event: any) {
- // 找到对应的文档
- const docIndex = this.documentSessions.findIndex(doc => doc.sessionId === sessionId);
- if (docIndex === -1) {
- // 未找到对应文档,可能是其他事件
- return;
- }
-
- const payload = event.payload;
- console.log(`处理文档事件,会话ID: ${sessionId}, 类型: ${payload.type}`);
-
- // 提取内容
- let content = '';
- const properties = payload.properties || {};
-
- if (payload.type === 'message.updated') {
- // 处理消息更新事件
- const messageInfo = properties.info || {};
- 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;
- }
-
- if (content) {
- // 更新文档内容
- this.documentSessions[docIndex] = {
- ...this.documentSessions[docIndex],
- content: content,
- lastUpdated: new Date(),
- hasContent: true,
- isLoading: false
- };
-
- // 触发变更检测(使用展开操作符创建新数组)
- this.documentSessions = [...this.documentSessions];
-
- console.log(`文档 ${this.documentSessions[docIndex].title} 内容已更新,长度: ${content.length} 字符`);
- }
- }
-
- /**
- * 获取所有文档会话ID(过滤空值)
- */
- getDocumentSessionIds(): string[] {
- return this.documentSessions
- .map(doc => doc.sessionId)
- .filter(sessionId => sessionId && sessionId.trim() !== '');
- }
-
- /**
- * 更新有效的实例ID
- * 优先使用传入的instanceId,其次使用项目会话ID,最后生成一个唯一ID
- */
- private updateEffectiveInstanceId(): void {
- if (this.instanceId && this.instanceId.trim() !== '') {
- // 使用传入的instanceId
- this.effectiveInstanceId = this.instanceId;
- console.log('🔍 [ProjectTabComponent] 使用传入的instanceId:', this.effectiveInstanceId);
- } else {
- // 生成唯一的实例ID作为回退,确保每个标签页有唯一ID
- this.effectiveInstanceId = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
- console.log('🔍 [ProjectTabComponent] 生成唯一instanceId:', this.effectiveInstanceId);
- }
- }
-
- ngOnDestroy() {
- this.subscriptions.unsubscribe();
-
- // 取消文档事件订阅
- if (this.eventSubscription) {
- this.eventSubscription.unsubscribe();
- this.eventSubscription = undefined;
- }
- }
- }
|