Procházet zdrojové kódy

添加项目管理api

qdy před 2 týdny
rodič
revize
9c690e9c91

+ 0
- 44
ng-code.log Zobrazit soubor

@@ -1,44 +0,0 @@
1
-
2
-> ng-code@0.0.0 start
3
-> ng serve
4
-
5
-Component HMR has been enabled, see https://angular.dev/hmr for more info.
6
-❯ Building...
7
-✔ Building...
8
-Initial chunk files | Names         |  Raw size
9
-scripts.js          | scripts       | 566.47 kB | 
10
-styles.css          | styles        | 386.31 kB | 
11
-main.js             | main          | 303.30 kB | 
12
-polyfills.js        | polyfills     |  89.77 kB | 
13
-
14
-                    | Initial total |   1.35 MB
15
-
16
-Application bundle generation complete. [4.173 seconds]
17
-
18
-Watch mode enabled. Watching for file changes...
19
-NOTE: Raw file sizes do not reflect development server per-request transformations.
20
-  ➜  Local:   http://localhost:4200/
21
-❯ Changes detected. Rebuilding...
22
-✔ Changes detected. Rebuilding...
23
-Initial chunk files | Names |  Raw size
24
-main.js             | main  | 303.26 kB | 
25
-
26
-Application bundle generation complete. [3.544 seconds]
27
-
28
-Page reload sent to client(s).
29
-❯ Changes detected. Rebuilding...
30
-✔ Changes detected. Rebuilding...
31
-Initial chunk files | Names |  Raw size
32
-main.js             | main  | 303.23 kB | 
33
-
34
-Application bundle generation complete. [1.331 seconds]
35
-
36
-Page reload sent to client(s).
37
-❯ Changes detected. Rebuilding...
38
-✔ Changes detected. Rebuilding...
39
-Initial chunk files | Names |  Raw size
40
-main.js             | main  | 303.24 kB | 
41
-
42
-Application bundle generation complete. [1.370 seconds]
43
-
44
-Page reload sent to client(s).

+ 44
- 31
src/app/app.component.html Zobrazit soubor

@@ -1,7 +1,7 @@
1 1
 @if (isLoginPage) {
2 2
   <router-outlet></router-outlet>
3 3
 } @else {
4
-  <!-- 多实例标签页布局 -->
4
+  <!-- 项目标签页布局 -->
5 5
   <div class="app-container">
6 6
     <!-- 顶部导航栏 -->
7 7
     <header class="app-header">
@@ -11,16 +11,32 @@
11 11
           <h1 class="logo-text">AGI对话系统</h1>
12 12
         </div>
13 13
         
14
-        <!-- 菜单项 -->
15
-        <nav class="menu-nav">
16
-          @for (menuItem of topMenuItems; track menuItem.id) {
17
-            <button mat-button 
18
-                    class="menu-button"
19
-                    (click)="openOrActivateInstance(menuItem.id)"
20
-                    [class.active]="isInstanceActiveForMenuItem(menuItem.id)">
21
-              <mat-icon>{{ menuItem.icon || 'folder' }}</mat-icon>
22
-              <span>{{ menuItem.name }}</span>
23
-            </button>
14
+        <!-- 主页按钮 -->
15
+        <button mat-button 
16
+                class="menu-button"
17
+                routerLink="/home"
18
+                routerLinkActive="active"
19
+                (click)="goHome()">
20
+          <mat-icon>home</mat-icon>
21
+          <span>主页</span>
22
+        </button>
23
+        
24
+
25
+        
26
+        <!-- 项目标签页 -->
27
+        <nav class="tabs-nav">
28
+          @for (tab of (tabs$ | async); track tab.id) {
29
+            <div class="tab-item" 
30
+                 [class.active]="(activeTabId$ | async) === tab.id"
31
+                 (click)="openProjectTab(tab.id)">
32
+              <mat-icon class="tab-icon">folder</mat-icon>
33
+              <span class="tab-title">{{ tab.title || '未命名项目' }}</span>
34
+              <button mat-icon-button 
35
+                      class="tab-close"
36
+                      (click)="closeProjectTab(tab.id, $event)">
37
+                <mat-icon>close</mat-icon>
38
+              </button>
39
+            </div>
24 40
           }
25 41
         </nav>
26 42
       </div>
@@ -38,6 +54,14 @@
38 54
         
39 55
         <!-- 用户信息 -->
40 56
         <div class="user-info">
57
+          <!-- 新建项目按钮(加号) -->
58
+          <button mat-icon-button 
59
+                  class="new-project-btn"
60
+                  (click)="openNewProjectModal()"
61
+                  matTooltip="新建项目">
62
+            <mat-icon>add</mat-icon>
63
+          </button>
64
+          
41 65
           <span class="username">{{ username }}</span>
42 66
           <button mat-icon-button [matMenuTriggerFor]="userMenu">
43 67
             <mat-icon>account_circle</mat-icon>
@@ -54,26 +78,15 @@
54 78
     
55 79
     <!-- 主内容区域 -->
56 80
     <main class="app-main">
57
-      @if (isInstancesLoading) {
58
-        <div class="loading-state">
59
-          <mat-spinner diameter="40"></mat-spinner>
60
-          <p>正在加载实例...</p>
61
-        </div>
62
-      } @else {
63
-        <!-- 当前活动实例 -->
64
-        @if (getActiveInstance(); as activeInstance) {
65
-          <div class="active-instance-container">
66
-            <app-instance [instanceId]="activeInstance.id"></app-instance>
67
-          </div>
68
-        } @else {
69
-          <!-- 空状态 -->
70
-          <div class="empty-state">
71
-            <mat-icon class="empty-icon">chat</mat-icon>
72
-            <h3>暂无实例</h3>
73
-            <p>点击上方菜单项创建实例</p>
74
-          </div>
75
-        }
76
-      }
81
+      <router-outlet></router-outlet>
77 82
     </main>
78 83
   </div>
84
+  
85
+  <!-- 新建项目模态框 -->
86
+  @if (showNewProjectModal) {
87
+    <app-new-project-modal
88
+      [(visible)]="showNewProjectModal"
89
+      (projectCreated)="onProjectCreated($event)">
90
+    </app-new-project-modal>
91
+  }
79 92
 }

+ 118
- 15
src/app/app.component.scss Zobrazit soubor

@@ -130,21 +130,34 @@
130 130
   }
131 131
 }
132 132
 
133
-.user-info {
134
-  display: flex;
135
-  align-items: center;
136
-  gap: 10px;
137
-  
138
-  .username {
139
-    font-size: 14px;
140
-    color: #4b5563;
141
-    font-weight: 500;
142
-  }
143
-  
144
-  mat-icon {
145
-    color: #6b7280;
146
-  }
147
-}
133
+ .user-info {
134
+   display: flex;
135
+   align-items: center;
136
+   gap: 10px;
137
+   
138
+   .new-project-btn {
139
+     margin-right: 8px;
140
+     
141
+     mat-icon {
142
+       color: #3b82f6;
143
+       transition: color 0.2s;
144
+     }
145
+     
146
+     &:hover mat-icon {
147
+       color: #2563eb;
148
+     }
149
+   }
150
+   
151
+   .username {
152
+     font-size: 14px;
153
+     color: #4b5563;
154
+     font-weight: 500;
155
+   }
156
+   
157
+   mat-icon {
158
+     color: #6b7280;
159
+   }
160
+ }
148 161
 
149 162
 /* 主内容区域 */
150 163
 .app-main {
@@ -263,4 +276,94 @@
263 276
 
264 277
 ::-webkit-scrollbar-thumb:hover {
265 278
   background: #a8a8a8;
279
+}
280
+
281
+/* 标签页导航 */
282
+.tabs-nav {
283
+  display: flex;
284
+  gap: 4px;
285
+  align-items: center;
286
+  margin-left: 16px;
287
+  overflow-x: auto;
288
+  max-width: 600px;
289
+  padding: 4px 0;
290
+  
291
+  &::-webkit-scrollbar {
292
+    height: 4px;
293
+  }
294
+  
295
+  &::-webkit-scrollbar-thumb {
296
+    background: #d1d5db;
297
+  }
298
+}
299
+
300
+.tab-item {
301
+  display: flex;
302
+  align-items: center;
303
+  gap: 6px;
304
+  padding: 6px 12px;
305
+  border-radius: 6px;
306
+  background: #f3f4f6;
307
+  border: 1px solid #e5e7eb;
308
+  cursor: pointer;
309
+  transition: all 0.2s;
310
+  white-space: nowrap;
311
+  flex-shrink: 0;
312
+  
313
+  .tab-icon {
314
+    font-size: 16px;
315
+    height: 16px;
316
+    width: 16px;
317
+    color: #6b7280;
318
+  }
319
+  
320
+  .tab-title {
321
+    font-size: 13px;
322
+    font-weight: 500;
323
+    color: #4b5563;
324
+    max-width: 120px;
325
+    overflow: hidden;
326
+    text-overflow: ellipsis;
327
+  }
328
+  
329
+  .tab-close {
330
+    width: 18px;
331
+    height: 18px;
332
+    line-height: 18px;
333
+    margin-left: 2px;
334
+    
335
+    mat-icon {
336
+      font-size: 14px;
337
+      height: 14px;
338
+      width: 14px;
339
+      color: #9ca3af;
340
+    }
341
+    
342
+    &:hover mat-icon {
343
+      color: #ef4444;
344
+    }
345
+  }
346
+  
347
+  &:hover {
348
+    background: #e5e7eb;
349
+    border-color: #d1d5db;
350
+  }
351
+  
352
+  &.active {
353
+    background: #3b82f6;
354
+    border-color: #3b82f6;
355
+    
356
+    .tab-icon,
357
+    .tab-title {
358
+      color: white;
359
+    }
360
+    
361
+    .tab-close mat-icon {
362
+      color: rgba(255, 255, 255, 0.8);
363
+    }
364
+    
365
+    .tab-close:hover mat-icon {
366
+      color: white;
367
+    }
368
+  }
266 369
 }

+ 84
- 162
src/app/app.component.ts Zobrazit soubor

@@ -1,70 +1,58 @@
1
-import { Component, OnInit, HostListener, OnDestroy } from '@angular/core';
1
+import { Component, OnInit, OnDestroy } from '@angular/core';
2 2
 import { CommonModule } from '@angular/common';
3
-import { Router, RouterOutlet } from '@angular/router';
3
+import { Router, RouterLink, RouterOutlet } from '@angular/router';
4
+import { Observable } from 'rxjs';
4 5
 import { MatIcon } from '@angular/material/icon';
5 6
 import { MatButtonModule } from '@angular/material/button';
6 7
 import { MatMenuModule } from '@angular/material/menu';
7 8
 import { MatTooltipModule } from '@angular/material/tooltip';
8 9
 import { MatTabsModule } from '@angular/material/tabs';
9 10
 import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
10
-import { DragDropModule } from '@angular/cdk/drag-drop';
11 11
 import { AuthService } from './services/auth.service';
12 12
 import { EventService } from './services/event.service';
13
-import { InstanceService } from './services/instance.service';
14
-import { MenuService } from './services/menu.service';
15
-import { InstanceComponent } from './components/instance.component';
13
+import { TabService } from './services/tab.service';
14
+import { SessionService } from './services/session.service';
16 15
 import { Subscription } from 'rxjs';
17 16
 import { ConfigService } from 'base-core';
18
-import { MenuItem } from './models/menu.model';
17
+import { NewProjectModalComponent } from './components/new-project-modal.component';
19 18
 
20 19
 @Component({
21 20
   selector: 'app-root',
22
-  imports: [CommonModule, RouterOutlet, InstanceComponent, MatIcon, MatButtonModule, MatMenuModule, DragDropModule, MatTooltipModule, MatProgressSpinnerModule],
21
+  imports: [CommonModule, RouterOutlet, RouterLink, MatIcon, MatButtonModule, MatMenuModule, MatTabsModule, MatTooltipModule, MatProgressSpinnerModule, NewProjectModalComponent],
23 22
   templateUrl: './app.component.html',
24 23
   styleUrl: './app.component.scss'
25 24
 })
26 25
 export class AppComponent implements OnInit, OnDestroy {
27
-  // 多实例管理相关属性
28
-  topMenuItems: MenuItem[] = [];
29
-  instances$: any; // 在构造函数中初始化
30
-  activeInstanceId$: any; // 在构造函数中初始化
31
-  isInstancesLoading = false;
26
+  // 项目标签页管理
27
+  tabs$: Observable<any>;
28
+  activeTabId$: Observable<string | null>;
29
+  showNewProjectModal = false;
32 30
   
33
-  // 布局相关属性(保留向后兼容)
34
-  leftWidth = 320; // 左侧会话列表宽度
35
-  rightWidth = 400; // 右侧日志区域宽度
36
-  isDraggingLeft = false;
37
-  isDraggingRight = false;
38
-  minLeftWidth = 200;
39
-  maxLeftWidth = 500;
40
-  minRightWidth = 300;
41
-  maxRightWidth = 600;
42 31
   private subscriptions: Subscription = new Subscription();
43 32
   isEventStreamConnected = false;
44 33
 
45
-   constructor(
34
+  constructor(
46 35
     private router: Router,
47 36
     public authService: AuthService,
48 37
     private config: ConfigService,
49 38
     private eventService: EventService,
50
-    private instanceService: InstanceService,
51
-    private menuService: MenuService
39
+    private tabService: TabService,
40
+    private sessionService: SessionService
52 41
   ) {
53
-    this.instances$ = this.instanceService.instances$;
54
-    this.activeInstanceId$ = this.instanceService.activeInstanceId$;
42
+    this.tabs$ = this.tabService.tabs$;
43
+    this.activeTabId$ = this.tabService.activeTabId$;
55 44
   }
56 45
   
57 46
   ngOnInit() {
58 47
     console.log('AppComponent初始化');
59
-    console.log('当前路由:', this.router.url);
60 48
     
61
-     // 禁用模拟数据,使用真实API
62
-     this.config.useMockData = false;
63
-     this.config.apiBaseUrl = '/api'; // 确保使用代理路径
64
-     console.log('使用真实API,配置:', {
65
-       useMockData: this.config.useMockData,
66
-       apiBaseUrl: this.config.apiBaseUrl
67
-     });
49
+    // 禁用模拟数据,使用真实API
50
+    this.config.useMockData = false;
51
+    this.config.apiBaseUrl = '/api';
52
+    console.log('使用真实API,配置:', {
53
+      useMockData: this.config.useMockData,
54
+      apiBaseUrl: this.config.apiBaseUrl
55
+    });
68 56
     
69 57
     // 订阅事件流连接状态
70 58
     this.subscriptions.add(
@@ -74,52 +62,50 @@ export class AppComponent implements OnInit, OnDestroy {
74 62
       })
75 63
     );
76 64
 
77
-    // 如果已登录,加载菜单和实例
65
+    // 如果已登录,加载项目
78 66
     if (this.authService.isAuthenticated()) {
79
-      this.loadMenuAndInstances();
67
+      this.loadProjects();
80 68
     } else {
81 69
       // 订阅认证状态变化
82 70
       this.subscriptions.add(
83 71
         this.authService.authState$.subscribe(authState => {
84 72
           if (authState.isAuthenticated) {
85
-            console.log('用户已认证,加载菜单和实例');
86
-            this.loadMenuAndInstances();
73
+            console.log('用户已认证,加载项目');
74
+            this.loadProjects();
87 75
           }
88 76
         })
89 77
       );
90 78
     }
91 79
   }
92 80
 
93
-  // 加载菜单并初始化实例
94
-  loadMenuAndInstances() {
95
-    this.isInstancesLoading = true;
96
-    
97
-    // 加载顶部菜单
98
-    this.menuService.getTopMenu().subscribe({
99
-      next: (menuItems) => {
100
-        this.topMenuItems = menuItems;
101
-        console.log('加载菜单项:', menuItems.length);
102
-        console.log('菜单项详细信息:', JSON.stringify(menuItems, null, 2));
103
-        if (menuItems.length > 0) {
104
-          console.log('第一个菜单项:', menuItems[0]);
105
-          console.log('第一个菜单项ID:', menuItems[0].id);
106
-        }
81
+  // 加载项目列表
82
+  loadProjects() {
83
+    this.sessionService.getSessions().subscribe({
84
+      next: (sessions) => {
85
+        // 从会话中提取项目信息
86
+        const projects = sessions
87
+          .filter(session => session.project_id)
88
+          .map(session => ({
89
+            id: session.project_id!,
90
+            title: session.title,
91
+            description: session.description,
92
+            agent: session.agent_name,
93
+            created_at: session.created_at,
94
+            updated_at: session.updated_at
95
+          }));
107 96
         
108
-        // 为每个菜单项创建实例
109
-        this.instanceService.initializeInstances().subscribe({
110
-          next: () => {
111
-            this.isInstancesLoading = false;
112
-            console.log('实例初始化完成');
113
-          },
114
-          error: (error) => {
115
-            console.error('实例初始化失败:', error);
116
-            this.isInstancesLoading = false;
117
-          }
118
-        });
97
+        // 去重:相同project_id只保留最新
98
+        const uniqueProjects = Array.from(
99
+          new Map(projects.map(p => [p.id, p])).values()
100
+        );
101
+        
102
+        console.log('加载项目列表:', uniqueProjects.length);
103
+        
104
+        // 可以在这里更新标签页服务中的项目数据(如果需要)
105
+        // this.tabService.updateProjects(uniqueProjects);
119 106
       },
120 107
       error: (error) => {
121
-        console.error('加载菜单失败:', error);
122
-        this.isInstancesLoading = false;
108
+        console.error('加载项目失败:', error);
123 109
       }
124 110
     });
125 111
   }
@@ -128,122 +114,58 @@ export class AppComponent implements OnInit, OnDestroy {
128 114
     return this.router.url.includes('/login');
129 115
   }
130 116
 
131
-  startLeftDrag(event: MouseEvent) {
132
-    event.preventDefault();
133
-    this.isDraggingLeft = true;
134
-    document.body.style.cursor = 'col-resize';
135
-    document.body.style.userSelect = 'none';
136
-  }
137
-
138
-  startRightDrag(event: MouseEvent) {
139
-    event.preventDefault();
140
-    this.isDraggingRight = true;
141
-    document.body.style.cursor = 'col-resize';
142
-    document.body.style.userSelect = 'none';
143
-  }
144
-
145
-  @HostListener('document:mousemove', ['$event'])
146
-  onDrag(event: MouseEvent) {
147
-    // 处理左侧拖动
148
-    if (this.isDraggingLeft) {
149
-      const newWidth = event.clientX;
150
-      if (newWidth >= this.minLeftWidth && newWidth <= this.maxLeftWidth) {
151
-        this.leftWidth = newWidth;
152
-      }
153
-    }
154
-    // 处理右侧拖动
155
-    if (this.isDraggingRight) {
156
-      // 右侧宽度 = 窗口宽度 - 鼠标X坐标 - 分割线宽度(8px)
157
-      const newRightWidth = window.innerWidth - event.clientX - 8;
158
-      if (newRightWidth >= this.minRightWidth && newRightWidth <= this.maxRightWidth) {
159
-        this.rightWidth = newRightWidth;
160
-      }
161
-    }
117
+  // 导航到主页
118
+  goHome() {
119
+    this.router.navigate(['/home']);
120
+    this.tabService.setActiveTab(null);
162 121
   }
163 122
 
164
-  @HostListener('document:mouseup')
165
-  stopDrag() {
166
-    if (this.isDraggingLeft) {
167
-      this.isDraggingLeft = false;
168
-      localStorage.setItem('leftWidth', this.leftWidth.toString());
169
-    }
170
-    if (this.isDraggingRight) {
171
-      this.isDraggingRight = false;
172
-      localStorage.setItem('rightWidth', this.rightWidth.toString());
173
-    }
174
-    document.body.style.cursor = '';
175
-    document.body.style.userSelect = '';
123
+  // 打开项目标签页
124
+  openProjectTab(projectId: string) {
125
+    this.router.navigate(['/project', projectId]);
126
+    this.tabService.setActiveTab(projectId);
176 127
   }
177 128
 
178
-  logout() {
179
-    this.authService.logout();
180
-  }
181
-
182
-  // 打开或激活实例
183
-  openOrActivateInstance(menuItemId: string) {
184
-    const menuItem = this.topMenuItems.find(item => item.id === menuItemId);
185
-    if (!menuItem) {
186
-      console.error('菜单项不存在:', menuItemId);
187
-      return;
188
-    }
189
-
190
-    // 检查是否已存在该菜单项的实例
191
-    const existingInstance = this.instanceService.getInstanceByMenuItemId(menuItemId);
192
-    if (existingInstance) {
193
-      // 激活现有实例
194
-      this.instanceService.setActiveInstance(existingInstance.id);
195
-      console.log('激活现有实例:', existingInstance.title);
196
-    } else {
197
-      // 创建新实例
198
-      this.instanceService.createInstance({
199
-        menuItemId: menuItemId,
200
-        title: menuItem.name
201
-      }).subscribe({
202
-        next: (instance) => {
203
-          console.log('创建新实例:', instance.title);
204
-        },
205
-        error: (error) => {
206
-          console.error('创建实例失败:', error);
207
-        }
208
-      });
209
-    }
210
-  }
211
-
212
-  // 关闭实例
213
-  closeInstance(instanceId: string, event?: MouseEvent) {
129
+  // 关闭项目标签页
130
+  closeProjectTab(projectId: string, event?: MouseEvent) {
214 131
     if (event) {
215 132
       event.stopPropagation();
216 133
       event.preventDefault();
217 134
     }
218 135
     
219
-    this.instanceService.closeInstance(instanceId);
220
-    console.log('关闭实例:', instanceId);
136
+    this.tabService.closeTab(projectId);
137
+    
138
+    // 如果关闭的是当前活动标签页,导航到主页
139
+    this.tabService.activeTabId$.subscribe(activeId => {
140
+      if (activeId === projectId) {
141
+        this.router.navigate(['/home']);
142
+      }
143
+    }).unsubscribe();
144
+    
145
+    console.log('关闭项目标签页:', projectId);
221 146
   }
222 147
 
223
-  // 获取实例标题
224
-  getInstanceTitle(instanceId: string): string {
225
-    const instance = this.instanceService.getInstanceById(instanceId);
226
-    return instance?.title || '未知实例';
148
+  // 打开新建项目模态框
149
+  openNewProjectModal() {
150
+    console.log('🔍 [AppComponent] 打开新建项目模态框,当前状态:', this.showNewProjectModal);
151
+    this.showNewProjectModal = true;
152
+    console.log('🔍 [AppComponent] 设置后状态:', this.showNewProjectModal);
227 153
   }
228 154
 
229
-  get username(): string {
230
-    return this.authService.getCurrentUser()?.username || '用户';
155
+  // 处理项目创建
156
+  onProjectCreated(project: any) {
157
+    this.openProjectTab(project.id);
231 158
   }
232 159
 
233
-  // 检查菜单项对应的实例是否处于活动状态
234
-  isInstanceActiveForMenuItem(menuItemId: string): boolean {
235
-    const instance = this.instanceService.getInstanceByMenuItemId(menuItemId);
236
-    const activeInstance = this.instanceService.getActiveInstance();
237
-    return instance && activeInstance ? instance.id === activeInstance.id : false;
160
+  logout() {
161
+    this.authService.logout();
238 162
   }
239 163
 
240
-  // 获取当前活动实例
241
-  getActiveInstance() {
242
-    return this.instanceService.getActiveInstance();
164
+  get username(): string {
165
+    return this.authService.getCurrentUser()?.username || '用户';
243 166
   }
244 167
 
245 168
   ngOnDestroy() {
246 169
     this.subscriptions.unsubscribe();
247 170
   }
248
-
249 171
 }

+ 6
- 3
src/app/app.routes.ts Zobrazit soubor

@@ -1,10 +1,13 @@
1 1
 import { Routes } from '@angular/router';
2 2
 import { LoginComponent } from './pages/login/login.component';
3
-import { DummyComponent } from './components/dummy.component';
3
+import { HomeComponent } from './components/home.component';
4
+import { ProjectTabComponent } from './components/project-tab.component';
4 5
 import { authGuard } from './guards/auth.guard';
5 6
 
6 7
 export const routes: Routes = [
7 8
   { path: 'login', component: LoginComponent },
8
-  { path: '', component: DummyComponent, canActivate: [authGuard] },
9
-  { path: '**', component: DummyComponent, canActivate: [authGuard] }
9
+  { path: 'home', component: HomeComponent, canActivate: [authGuard] },
10
+  { path: 'project/:projectId', component: ProjectTabComponent, canActivate: [authGuard] },
11
+  { path: '', redirectTo: '/home', pathMatch: 'full' },
12
+  { path: '**', redirectTo: '/home' }
10 13
 ];

+ 464
- 0
src/app/components/home.component.ts Zobrazit soubor

@@ -0,0 +1,464 @@
1
+import { Component, OnInit, OnDestroy } from '@angular/core';
2
+import { CommonModule } from '@angular/common';
3
+import { MatCardModule } from '@angular/material/card';
4
+import { MatButtonModule } from '@angular/material/button';
5
+import { MatIconModule } from '@angular/material/icon';
6
+import { MatChipsModule } from '@angular/material/chips';
7
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
8
+import { MatTooltipModule } from '@angular/material/tooltip';
9
+import { Subscription } from 'rxjs';
10
+import { SessionService } from '../services/session.service';
11
+import { TabService } from '../services/tab.service';
12
+import { AgentService } from '../services/agent.service';
13
+import { EventService } from '../services/event.service';
14
+import { Session } from '../models/session.model';
15
+
16
+@Component({
17
+  selector: 'app-home',
18
+  standalone: true,
19
+  imports: [
20
+    CommonModule,
21
+    MatCardModule,
22
+    MatButtonModule,
23
+    MatIconModule,
24
+    MatChipsModule,
25
+    MatProgressSpinnerModule,
26
+    MatTooltipModule
27
+  ],
28
+  template: `
29
+    <div class="home-container">
30
+      <!-- 顶部工具栏 -->
31
+      <div class="toolbar">
32
+        <h1>项目列表</h1>
33
+      </div>
34
+      
35
+      <!-- 项目卡片网格 -->
36
+      @if (isLoading) {
37
+        <div class="loading-state">
38
+          <mat-spinner diameter="40"></mat-spinner>
39
+          <p>加载项目中...</p>
40
+        </div>
41
+      } @else if (projects.length === 0) {
42
+        <div class="empty-state">
43
+          <mat-icon class="empty-icon">inbox</mat-icon>
44
+          <h3>暂无项目</h3>
45
+          <p>点击"新建项目"按钮创建第一个项目</p>
46
+        </div>
47
+      } @else {
48
+        <div class="projects-grid">
49
+          @for (project of projects; track project.project_id) {
50
+            <mat-card class="project-card">
51
+              <mat-card-header>
52
+                <mat-card-title>{{ project.title }}</mat-card-title>
53
+                <mat-card-subtitle>
54
+                  <span class="agent-badge">{{ getAgentDisplayName(project.agent_name) }}</span>
55
+                  <span class="status-badge" [class]="getStatusClass(project.status)">
56
+                    {{ getStatusDisplayName(project.status) }}
57
+                  </span>
58
+                </mat-card-subtitle>
59
+              </mat-card-header>
60
+              
61
+              <mat-card-content>
62
+                @if (project.description) {
63
+                  <p class="project-description">{{ project.description }}</p>
64
+                }
65
+                
66
+                <div class="project-meta">
67
+                  <div class="meta-item">
68
+                    <mat-icon>calendar_today</mat-icon>
69
+                    <span>{{ formatDate(project.created_at) }}</span>
70
+                  </div>
71
+                  <div class="meta-item">
72
+                    <mat-icon>update</mat-icon>
73
+                    <span>{{ formatDate(project.updated_at) }}</span>
74
+                  </div>
75
+                </div>
76
+                
77
+                <!-- 最新AI对话预览 -->
78
+                <div class="log-preview">
79
+                  <mat-icon>chat</mat-icon>
80
+                  <span class="log-text">最新对话:{{ getProjectLatestMessage(project.project_id || project.id) }}</span>
81
+                </div>
82
+              </mat-card-content>
83
+              
84
+              <mat-card-actions>
85
+                <button mat-button color="primary" (click)="runProject(project)">
86
+                  <mat-icon>play_arrow</mat-icon>
87
+                  运行
88
+                </button>
89
+                <button mat-button (click)="editProject(project)">
90
+                  <mat-icon>edit</mat-icon>
91
+                  编辑
92
+                </button>
93
+                <button mat-button color="warn" (click)="deleteProject(project)">
94
+                  <mat-icon>delete</mat-icon>
95
+                  删除
96
+                </button>
97
+              </mat-card-actions>
98
+            </mat-card>
99
+          }
100
+        </div>
101
+      }
102
+    </div>
103
+  `,
104
+  styles: [`
105
+    .home-container {
106
+      padding: 24px;
107
+      max-width: 1200px;
108
+      margin: 0 auto;
109
+    }
110
+    
111
+    .toolbar {
112
+      display: flex;
113
+      justify-content: space-between;
114
+      align-items: center;
115
+      margin-bottom: 32px;
116
+    }
117
+    
118
+    .toolbar h1 {
119
+      margin: 0;
120
+      font-size: 1.8rem;
121
+      font-weight: 500;
122
+    }
123
+    
124
+    .loading-state {
125
+      text-align: center;
126
+      padding: 60px 20px;
127
+      color: #666;
128
+    }
129
+    
130
+    .empty-state {
131
+      text-align: center;
132
+      padding: 80px 20px;
133
+      color: #666;
134
+    }
135
+    
136
+    .empty-icon {
137
+      font-size: 72px;
138
+      height: 72px;
139
+      width: 72px;
140
+      margin-bottom: 20px;
141
+      color: #ccc;
142
+    }
143
+    
144
+    .projects-grid {
145
+      display: grid;
146
+      grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
147
+      gap: 24px;
148
+    }
149
+    
150
+    .project-card {
151
+      height: 100%;
152
+      display: flex;
153
+      flex-direction: column;
154
+    }
155
+    
156
+    .project-card mat-card-header {
157
+      margin-bottom: 16px;
158
+    }
159
+    
160
+    .agent-badge {
161
+      display: inline-block;
162
+      background: #e3f2fd;
163
+      color: #1976d2;
164
+      padding: 2px 8px;
165
+      border-radius: 12px;
166
+      font-size: 12px;
167
+      margin-right: 8px;
168
+    }
169
+    
170
+    .status-badge {
171
+      display: inline-block;
172
+      padding: 2px 8px;
173
+      border-radius: 12px;
174
+      font-size: 12px;
175
+    }
176
+    
177
+    .status-requirement_document { background: #fff3e0; color: #f57c00; }
178
+    .status-technical_document { background: #e8f5e8; color: #388e3c; }
179
+    .status-code { background: #e3f2fd; color: #1976d2; }
180
+    .status-test { background: #f3e5f5; color: #7b1fa2; }
181
+    .status-release { background: #e8f5e8; color: #388e3c; }
182
+    
183
+    .project-description {
184
+      color: #666;
185
+      line-height: 1.5;
186
+      margin-bottom: 16px;
187
+      display: -webkit-box;
188
+      -webkit-line-clamp: 3;
189
+      -webkit-box-orient: vertical;
190
+      overflow: hidden;
191
+    }
192
+    
193
+    .project-meta {
194
+      display: flex;
195
+      gap: 16px;
196
+      margin-bottom: 16px;
197
+      color: #888;
198
+      font-size: 14px;
199
+    }
200
+    
201
+    .meta-item {
202
+      display: flex;
203
+      align-items: center;
204
+      gap: 4px;
205
+    }
206
+    
207
+    .meta-item mat-icon {
208
+      font-size: 16px;
209
+      height: 16px;
210
+      width: 16px;
211
+    }
212
+    
213
+    .log-preview {
214
+      display: flex;
215
+      align-items: center;
216
+      gap: 8px;
217
+      padding: 8px;
218
+      background: #f5f5f5;
219
+      border-radius: 4px;
220
+      font-size: 12px;
221
+      color: #666;
222
+    }
223
+    
224
+    .log-preview mat-icon {
225
+      font-size: 16px;
226
+      height: 16px;
227
+      width: 16px;
228
+    }
229
+    
230
+    .log-text {
231
+      flex: 1;
232
+      overflow: hidden;
233
+      text-overflow: ellipsis;
234
+      white-space: nowrap;
235
+    }
236
+    
237
+    mat-card-actions {
238
+      margin-top: auto;
239
+      padding: 16px;
240
+    }
241
+  `]
242
+})
243
+export class HomeComponent implements OnInit, OnDestroy {
244
+  projects: Session[] = [];
245
+  isLoading = false;
246
+  showNewProjectModal = false;
247
+  
248
+  // 存储项目最新消息的映射(project_id -> 最新消息内容)
249
+  private projectMessages = new Map<string, string>();
250
+  
251
+  private subscriptions: Subscription = new Subscription();
252
+  
253
+  constructor(
254
+    private sessionService: SessionService,
255
+    private tabService: TabService,
256
+    private agentService: AgentService,
257
+    private eventService: EventService
258
+  ) {}
259
+  
260
+  ngOnInit() {
261
+    this.loadProjects();
262
+    
263
+    // 订阅会话更新事件,更新项目列表
264
+    this.subscriptions.add(
265
+      this.sessionService.sessions$.subscribe(sessions => {
266
+        this.updateProjects(sessions);
267
+      })
268
+    );
269
+    
270
+    // 订阅事件流,接收实时更新
271
+    this.subscriptions.add(
272
+      this.eventService.allEvents$.subscribe(event => {
273
+        const payload = event.payload;
274
+        
275
+        // 处理会话更新事件,更新对应项目卡片
276
+        if (payload.type === 'session.updated') {
277
+          this.refreshProjects();
278
+        }
279
+        
280
+        // 处理消息更新事件,更新项目卡片中的最新消息预览
281
+        if (payload.type === 'message.updated') {
282
+          this.handleMessageUpdated(event);
283
+        }
284
+        
285
+        // 处理消息部分更新事件(流式输出)
286
+        if (payload.type === 'message.part.updated') {
287
+          this.handleMessagePartUpdated(event);
288
+        }
289
+      })
290
+    );
291
+  }
292
+  
293
+  loadProjects() {
294
+    this.isLoading = true;
295
+    this.sessionService.loadSessions();
296
+  }
297
+  
298
+  updateProjects(sessions: Session[]) {
299
+    // 按project_id去重,获取唯一项目
300
+    const projectMap = new Map<string, Session>();
301
+    
302
+    sessions.forEach(session => {
303
+      const projectId = session.project_id || session.id;
304
+      // 保留最新或第一个会话(简单起见,保留第一个遇到的)
305
+      if (!projectMap.has(projectId)) {
306
+        projectMap.set(projectId, session);
307
+      }
308
+    });
309
+    
310
+    const newProjects = Array.from(projectMap.values());
311
+    this.projects = newProjects;
312
+    this.isLoading = false;
313
+    
314
+    // 清理不再存在的项目的消息缓存
315
+    this.cleanupProjectMessages(newProjects);
316
+  }
317
+  
318
+  // 清理无效项目的消息缓存
319
+  private cleanupProjectMessages(currentProjects: Session[]) {
320
+    const currentProjectIds = new Set(currentProjects.map(p => p.project_id || p.id));
321
+    
322
+    // 删除不再存在的项目的消息
323
+    for (const projectId of this.projectMessages.keys()) {
324
+      if (!currentProjectIds.has(projectId)) {
325
+        this.projectMessages.delete(projectId);
326
+      }
327
+    }
328
+  }
329
+  
330
+  refreshProjects() {
331
+    this.sessionService.loadSessions();
332
+  }
333
+  
334
+  getAgentDisplayName(agentId: string): string {
335
+    return this.agentService.getAgentDisplayName(agentId);
336
+  }
337
+  
338
+  getStatusDisplayName(status: string): string {
339
+    const statusMap: Record<string, string> = {
340
+      'requirement_document': '需求文档',
341
+      'technical_document': '技术文档',
342
+      'code': '代码开发',
343
+      'test': '测试',
344
+      'release': '发布'
345
+    };
346
+    return statusMap[status] || status;
347
+  }
348
+  
349
+  getStatusClass(status: string): string {
350
+    return `status-${status}`;
351
+  }
352
+  
353
+  formatDate(dateString?: string): string {
354
+    if (!dateString) return '未知';
355
+    const date = new Date(dateString);
356
+    return date.toLocaleDateString('zh-CN');
357
+  }
358
+  
359
+  runProject(project: Session) {
360
+    console.log('运行项目:', project.title);
361
+    // TODO: 发送需求文档开始工作
362
+    // 1. 打开项目页签
363
+    this.editProject(project);
364
+    // 2. 发送需求文档到AI(如果存在需求文档)
365
+    alert('运行功能待实现');
366
+  }
367
+  
368
+  editProject(project: Session) {
369
+    console.log('编辑项目:', project.title);
370
+    // 打开项目页签
371
+    const tabOpened = this.tabService.openProjectTab(project);
372
+    if (!tabOpened) {
373
+      alert('页签打开失败(可能达到页签数量限制)');
374
+    }
375
+  }
376
+  
377
+  deleteProject(project: Session) {
378
+    if (confirm(`确定删除项目 "${project.title}" 吗?`)) {
379
+      console.log('删除项目:', project.title);
380
+      // TODO: 调用删除API
381
+      alert('删除功能待实现');
382
+    }
383
+  }
384
+  
385
+  onProjectCreated(session: Session) {
386
+    console.log('新项目创建:', session.title);
387
+    // 刷新项目列表
388
+    this.refreshProjects();
389
+  }
390
+  
391
+  // 处理消息更新事件
392
+  private handleMessageUpdated(event: any) {
393
+    const payload = event.payload;
394
+    const messageInfo = payload.properties?.info;
395
+    
396
+    if (!messageInfo) return;
397
+    
398
+    // 提取会话ID(可能来自不同字段)
399
+    const sessionId = messageInfo.sessionID || messageInfo.sessionId || messageInfo.session_id;
400
+    if (!sessionId) return;
401
+    
402
+    // 查找对应的项目ID(会话ID可能等于项目ID,或需要映射)
403
+    // 这里简单假设会话ID就是项目ID,或者通过projects数组查找
404
+    const project = this.projects.find(p => p.id === sessionId || p.project_id === sessionId);
405
+    if (!project) return;
406
+    
407
+    const projectId = project.project_id || project.id;
408
+    const content = messageInfo.content || '';
409
+    
410
+    // 只存储AI消息(assistant角色)
411
+    if (messageInfo.role === 'assistant' && content.trim()) {
412
+      // 截断过长的消息
413
+      const truncatedContent = content.length > 100 ? content.substring(0, 100) + '...' : content;
414
+      this.projectMessages.set(projectId, truncatedContent);
415
+      console.log(`🔍 [HomeComponent] 更新项目 ${projectId} 的最新消息:`, truncatedContent);
416
+    }
417
+  }
418
+  
419
+  // 处理消息部分更新事件(流式输出)
420
+  private handleMessagePartUpdated(event: any) {
421
+    const payload = event.payload;
422
+    const part = payload.properties?.part;
423
+    const delta = payload.properties?.delta;
424
+    
425
+    if (!part || !delta) return;
426
+    
427
+    // 提取会话ID
428
+    const sessionId = part.sessionID || part.sessionId || part.session_id;
429
+    if (!sessionId) return;
430
+    
431
+    // 查找对应的项目
432
+    const project = this.projects.find(p => p.id === sessionId || p.project_id === sessionId);
433
+    if (!project) return;
434
+    
435
+    const projectId = project.project_id || project.id;
436
+    
437
+    // 只处理文本类型的部分更新
438
+    if (part.type === 'text' || part.type === 'reasoning') {
439
+      // 获取当前消息或初始化
440
+      const currentMessage = this.projectMessages.get(projectId) || '';
441
+      const newMessage = currentMessage + delta;
442
+      
443
+      // 截断过长的消息
444
+      const truncatedMessage = newMessage.length > 100 ? newMessage.substring(0, 100) + '...' : newMessage;
445
+      this.projectMessages.set(projectId, truncatedMessage);
446
+      
447
+      console.log(`🔍 [HomeComponent] 流式更新项目 ${projectId} 的消息:`, truncatedMessage.substring(0, 50));
448
+    }
449
+  }
450
+  
451
+  // 获取项目的最新消息预览
452
+  getProjectLatestMessage(projectId: string): string {
453
+    const message = this.projectMessages.get(projectId);
454
+    if (message) {
455
+      return message;
456
+    }
457
+    // 默认消息
458
+    return '项目已创建,等待需求文档';
459
+  }
460
+  
461
+  ngOnDestroy() {
462
+    this.subscriptions.unsubscribe();
463
+  }
464
+}

+ 244
- 0
src/app/components/new-project-modal.component.ts Zobrazit soubor

@@ -0,0 +1,244 @@
1
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
2
+import { CommonModule } from '@angular/common';
3
+import { FormsModule } from '@angular/forms';
4
+import { MatSelectModule } from '@angular/material/select';
5
+import { MatInputModule } from '@angular/material/input';
6
+import { MatFormFieldModule } from '@angular/material/form-field';
7
+import { MatButtonModule } from '@angular/material/button';
8
+import { MatIconModule } from '@angular/material/icon';
9
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
10
+import { AgentService } from '../services/agent.service';
11
+import { SessionService } from '../services/session.service';
12
+import { TabService } from '../services/tab.service';
13
+import { ProjectCreateRequest } from '../models/session.model';
14
+
15
+@Component({
16
+  selector: 'app-new-project-modal',
17
+  standalone: true,
18
+  imports: [
19
+    CommonModule,
20
+    FormsModule,
21
+    MatSelectModule,
22
+    MatInputModule,
23
+    MatFormFieldModule,
24
+    MatButtonModule,
25
+    MatIconModule,
26
+    MatProgressSpinnerModule
27
+  ],
28
+  template: `
29
+    @if (visible) {
30
+      <div class="modal-overlay" (click)="close()">
31
+        <div class="modal-container" (click)="$event.stopPropagation()">
32
+          <div class="modal-header">
33
+            <h2>新建项目</h2>
34
+            <button mat-icon-button (click)="close()">
35
+              <mat-icon>close</mat-icon>
36
+            </button>
37
+          </div>
38
+          
39
+          <div class="modal-content">
40
+            <form #projectForm="ngForm" (ngSubmit)="createProject()">
41
+              <!-- 智能体选择 -->
42
+              <mat-form-field appearance="outline" class="full-width">
43
+                <mat-label>选择智能体</mat-label>
44
+                <mat-select [(ngModel)]="projectData.agent_name" name="agent_name" required>
45
+                  @for (agent of agents; track agent.value) {
46
+                    <mat-option [value]="agent.value">{{ agent.label }}</mat-option>
47
+                  }
48
+                </mat-select>
49
+                <mat-hint>选择处理项目的智能体</mat-hint>
50
+              </mat-form-field>
51
+              
52
+              <!-- 项目标题 -->
53
+              <mat-form-field appearance="outline" class="full-width">
54
+                <mat-label>项目标题</mat-label>
55
+                <input matInput [(ngModel)]="projectData.title" name="title" required maxlength="100">
56
+                <mat-hint>输入项目标题,最多100个字符</mat-hint>
57
+              </mat-form-field>
58
+              
59
+              <!-- 项目描述 -->
60
+              <mat-form-field appearance="outline" class="full-width">
61
+                <mat-label>项目描述</mat-label>
62
+                <textarea matInput [(ngModel)]="projectData.description" name="description" rows="4"></textarea>
63
+                <mat-hint>详细描述项目需求和目标</mat-hint>
64
+              </mat-form-field>
65
+              
66
+              <div class="modal-actions">
67
+                <button mat-button type="button" (click)="close()">取消</button>
68
+                <button 
69
+                  mat-raised-button 
70
+                  color="primary" 
71
+                  type="submit"
72
+                  [disabled]="!projectForm.valid || isLoading"
73
+                >
74
+                  @if (isLoading) {
75
+                    <mat-spinner diameter="20"></mat-spinner>
76
+                  } @else {
77
+                    创建项目
78
+                  }
79
+                </button>
80
+              </div>
81
+            </form>
82
+          </div>
83
+        </div>
84
+      </div>
85
+    }
86
+  `,
87
+  styles: [`
88
+    .modal-overlay {
89
+      position: fixed;
90
+      top: 0;
91
+      left: 0;
92
+      right: 0;
93
+      bottom: 0;
94
+      background-color: rgba(0, 0, 0, 0.5);
95
+      display: flex;
96
+      align-items: center;
97
+      justify-content: center;
98
+      z-index: 1000;
99
+    }
100
+    
101
+    .modal-container {
102
+      background: white;
103
+      border-radius: 8px;
104
+      width: 500px;
105
+      max-width: 90vw;
106
+      max-height: 80vh;
107
+      overflow: auto;
108
+      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
109
+    }
110
+    
111
+    .modal-header {
112
+      display: flex;
113
+      justify-content: space-between;
114
+      align-items: center;
115
+      padding: 20px 24px;
116
+      border-bottom: 1px solid #e0e0e0;
117
+    }
118
+    
119
+    .modal-header h2 {
120
+      margin: 0;
121
+      font-size: 1.5rem;
122
+      font-weight: 500;
123
+    }
124
+    
125
+    .modal-content {
126
+      padding: 24px;
127
+    }
128
+    
129
+    .full-width {
130
+      width: 100%;
131
+      margin-bottom: 20px;
132
+    }
133
+    
134
+    .modal-actions {
135
+      display: flex;
136
+      justify-content: flex-end;
137
+      gap: 12px;
138
+      margin-top: 24px;
139
+    }
140
+    
141
+    mat-spinner {
142
+      display: inline-block;
143
+      margin-right: 8px;
144
+    }
145
+  `]
146
+})
147
+export class NewProjectModalComponent implements OnInit {
148
+  @Input() visible = false;
149
+  @Output() visibleChange = new EventEmitter<boolean>();
150
+  @Output() projectCreated = new EventEmitter<any>();
151
+  
152
+  agents: {value: string, label: string}[] = [];
153
+  projectData: ProjectCreateRequest = {
154
+    title: '',
155
+    agent_name: '',
156
+    description: ''
157
+  };
158
+  isLoading = false;
159
+  
160
+  constructor(
161
+    private agentService: AgentService,
162
+    private sessionService: SessionService,
163
+    private tabService: TabService
164
+  ) {}
165
+  
166
+  ngOnInit() {
167
+    this.loadAgents();
168
+  }
169
+  
170
+  loadAgents() {
171
+    this.agentService.getAgentOptions().subscribe({
172
+      next: (agents) => {
173
+        this.agents = agents;
174
+        // 设置默认智能体
175
+        if (agents.length > 0 && !this.projectData.agent_name) {
176
+          this.projectData.agent_name = agents[0].value;
177
+        }
178
+      },
179
+      error: (error) => {
180
+        console.error('加载智能体列表失败:', error);
181
+        // 使用本地映射作为备选
182
+        this.agents = Object.entries(this.agentService.getAgentDisplayName('')).map(([value, label]) => ({
183
+          value,
184
+          label
185
+        }));
186
+      }
187
+    });
188
+  }
189
+  
190
+  createProject() {
191
+    if (!this.projectData.title.trim() || !this.projectData.agent_name) {
192
+      return;
193
+    }
194
+    
195
+    this.isLoading = true;
196
+    
197
+    // 生成项目ID
198
+    const projectId = `proj_${crypto.randomUUID()}`;
199
+    const request: ProjectCreateRequest = {
200
+      ...this.projectData,
201
+      project_id: projectId
202
+    };
203
+    
204
+    this.sessionService.createProject(request).subscribe({
205
+      next: (session) => {
206
+        console.log('项目创建成功:', session);
207
+        this.isLoading = false;
208
+        
209
+        // 打开项目页签
210
+        const tabOpened = this.tabService.openProjectTab(session);
211
+        if (!tabOpened) {
212
+          // 页签打开失败(可能达到限制)
213
+          alert('项目创建成功,但页签打开失败(可能达到页签数量限制)');
214
+        }
215
+        
216
+        // 通知父组件
217
+        this.projectCreated.emit(session);
218
+        this.close();
219
+        
220
+        // 重置表单
221
+        this.resetForm();
222
+      },
223
+      error: (error) => {
224
+        console.error('创建项目失败:', error);
225
+        alert(`创建项目失败: ${error.message}`);
226
+        this.isLoading = false;
227
+      }
228
+    });
229
+  }
230
+  
231
+  close() {
232
+    this.visible = false;
233
+    this.visibleChange.emit(false);
234
+    this.resetForm();
235
+  }
236
+  
237
+  resetForm() {
238
+    this.projectData = {
239
+      title: '',
240
+      agent_name: this.agents.length > 0 ? this.agents[0].value : '',
241
+      description: ''
242
+    };
243
+  }
244
+}

+ 532
- 0
src/app/components/project-tab.component.ts Zobrazit soubor

@@ -0,0 +1,532 @@
1
+import { Component, OnInit, OnDestroy, HostListener, Input } from '@angular/core';
2
+import { CommonModule } from '@angular/common';
3
+import { MatIcon } from '@angular/material/icon';
4
+import { MatButtonModule } from '@angular/material/button';
5
+import { MatCardModule } from '@angular/material/card';
6
+import { MatListModule } from '@angular/material/list';
7
+import { MatExpansionModule } from '@angular/material/expansion';
8
+import { MatChipsModule } from '@angular/material/chips';
9
+import { MatProgressBarModule } from '@angular/material/progress-bar';
10
+import { MatTooltipModule } from '@angular/material/tooltip';
11
+import { Subscription } from 'rxjs';
12
+import { ConversationComponent } from './conversation.component';
13
+import { TabService, Tab } from '../services/tab.service';
14
+import { SessionService } from '../services/session.service';
15
+import { AgentService } from '../services/agent.service';
16
+import { Session, SessionStatus } from '../models/session.model';
17
+
18
+@Component({
19
+  selector: 'app-project-tab',
20
+  standalone: true,
21
+  imports: [
22
+    CommonModule,
23
+    MatIcon,
24
+    MatButtonModule,
25
+    MatCardModule,
26
+    MatListModule,
27
+    MatExpansionModule,
28
+    MatChipsModule,
29
+    MatProgressBarModule,
30
+    MatTooltipModule,
31
+    ConversationComponent
32
+  ],
33
+  template: `
34
+    <div class="project-tab-container" [style.gridTemplateColumns]="leftWidth + 'px 8px 1fr'">
35
+      <!-- 左侧面板:项目信息和步骤列表 -->
36
+      <div class="left-panel">
37
+        @if (project) {
38
+          <!-- 项目信息卡片 -->
39
+          <mat-card class="project-info-card">
40
+            <mat-card-header>
41
+              <mat-card-title>{{ project.title }}</mat-card-title>
42
+              <mat-card-subtitle>
43
+                <span class="agent-chip">{{ getAgentDisplayName(project.agent_name) }}</span>
44
+                <span class="status-chip" [class]="getStatusClass(project.status)">
45
+                  {{ getStatusDisplayName(project.status) }}
46
+                </span>
47
+              </mat-card-subtitle>
48
+            </mat-card-header>
49
+            
50
+            <mat-card-content>
51
+              @if (project.description) {
52
+                <p class="project-description">{{ project.description }}</p>
53
+              }
54
+              
55
+              <div class="project-meta">
56
+                <div class="meta-item">
57
+                  <mat-icon>person</mat-icon>
58
+                  <span>{{ project.user_id || '未知用户' }}</span>
59
+                </div>
60
+                <div class="meta-item">
61
+                  <mat-icon>calendar_today</mat-icon>
62
+                  <span>{{ formatDate(project.created_at) }}</span>
63
+                </div>
64
+              </div>
65
+              
66
+              <!-- 进度条 -->
67
+              <div class="progress-section">
68
+                <div class="progress-label">项目进度</div>
69
+                <mat-progress-bar 
70
+                  mode="determinate" 
71
+                  [value]="getProgressValue(project.status)">
72
+                </mat-progress-bar>
73
+                <div class="progress-text">{{ getProgressText(project.status) }}</div>
74
+              </div>
75
+              
76
+              <!-- 最新日志 -->
77
+              <div class="latest-log">
78
+                <div class="log-header">
79
+                  <mat-icon>info</mat-icon>
80
+                  <span>最新日志</span>
81
+                </div>
82
+                <div class="log-content">项目已创建,等待需求文档输入</div>
83
+              </div>
84
+            </mat-card-content>
85
+          </mat-card>
86
+          
87
+          <!-- 步骤列表 -->
88
+          <mat-accordion class="steps-accordion" multi>
89
+            <!-- 需求文档 -->
90
+            <mat-expansion-panel [expanded]="project.status === 'requirement_document'">
91
+              <mat-expansion-panel-header>
92
+                <mat-panel-title>
93
+                  <mat-icon>description</mat-icon>
94
+                  <span>需求文档</span>
95
+                </mat-panel-title>
96
+                <mat-panel-description>
97
+                  @if (project.status === 'requirement_document') {
98
+                    <span class="current-step">当前步骤</span>
99
+                  }
100
+                </mat-panel-description>
101
+              </mat-expansion-panel-header>
102
+              <div class="step-content">
103
+                <p>详细描述项目需求和目标</p>
104
+                <button mat-button color="primary" (click)="focusRequirementInput()">
105
+                  <mat-icon>edit</mat-icon>
106
+                  编辑需求
107
+                </button>
108
+              </div>
109
+            </mat-expansion-panel>
110
+            
111
+            <!-- 技术文档 -->
112
+            <mat-expansion-panel [expanded]="project.status === 'technical_document'">
113
+              <mat-expansion-panel-header>
114
+                <mat-panel-title>
115
+                  <mat-icon>code</mat-icon>
116
+                  <span>技术文档</span>
117
+                </mat-panel-title>
118
+                <mat-panel-description>
119
+                  @if (project.status === 'technical_document') {
120
+                    <span class="current-step">当前步骤</span>
121
+                  }
122
+                </mat-panel-description>
123
+              </mat-expansion-panel-header>
124
+              <div class="step-content">
125
+                <p>技术架构和实现方案</p>
126
+                <div class="step-status">待完成</div>
127
+              </div>
128
+            </mat-expansion-panel>
129
+            
130
+            <!-- 代码开发 -->
131
+            <mat-expansion-panel [expanded]="project.status === 'code'">
132
+              <mat-expansion-panel-header>
133
+                <mat-panel-title>
134
+                  <mat-icon>terminal</mat-icon>
135
+                  <span>代码开发</span>
136
+                </mat-panel-title>
137
+                <mat-panel-description>
138
+                  @if (project.status === 'code') {
139
+                    <span class="current-step">当前步骤</span>
140
+                  }
141
+                </mat-panel-description>
142
+              </mat-expansion-panel-header>
143
+              <div class="step-content">
144
+                <p>自动生成的代码步骤</p>
145
+                <mat-list dense>
146
+                  <mat-list-item>
147
+                    <mat-icon matListItemIcon>looks_one</mat-icon>
148
+                    <div matListItemTitle>步骤1: 数据模型设计</div>
149
+                  </mat-list-item>
150
+                  <mat-list-item>
151
+                    <mat-icon matListItemIcon>looks_two</mat-icon>
152
+                    <div matListItemTitle>步骤2: API接口开发</div>
153
+                  </mat-list-item>
154
+                  <mat-list-item>
155
+                    <mat-icon matListItemIcon>looks_3</mat-icon>
156
+                    <div matListItemTitle>步骤3: 前端页面实现</div>
157
+                  </mat-list-item>
158
+                </mat-list>
159
+              </div>
160
+            </mat-expansion-panel>
161
+            
162
+            <!-- 测试 -->
163
+            <mat-expansion-panel [expanded]="project.status === 'test'">
164
+              <mat-expansion-panel-header>
165
+                <mat-panel-title>
166
+                  <mat-icon>bug_report</mat-icon>
167
+                  <span>测试</span>
168
+                </mat-panel-title>
169
+                <mat-panel-description>
170
+                  @if (project.status === 'test') {
171
+                    <span class="current-step">当前步骤</span>
172
+                  }
173
+                </mat-panel-description>
174
+              </mat-expansion-panel-header>
175
+              <div class="step-content">
176
+                <p>功能测试和验证</p>
177
+                <div class="step-status">待完成</div>
178
+              </div>
179
+            </mat-expansion-panel>
180
+            
181
+            <!-- 发布 -->
182
+            <mat-expansion-panel [expanded]="project.status === 'release'">
183
+              <mat-expansion-panel-header>
184
+                <mat-panel-title>
185
+                  <mat-icon>rocket_launch</mat-icon>
186
+                  <span>发布</span>
187
+                </mat-panel-title>
188
+                <mat-panel-description>
189
+                  @if (project.status === 'release') {
190
+                    <span class="current-step">当前步骤</span>
191
+                  }
192
+                </mat-panel-description>
193
+              </mat-expansion-panel-header>
194
+              <div class="step-content">
195
+                <p>部署和发布上线</p>
196
+                <div class="step-status">待完成</div>
197
+              </div>
198
+            </mat-expansion-panel>
199
+          </mat-accordion>
200
+        } @else {
201
+          <div class="no-project">
202
+            <mat-icon>error</mat-icon>
203
+            <p>未找到项目信息</p>
204
+          </div>
205
+        }
206
+      </div>
207
+      
208
+      <!-- 分割线(调整左侧宽度) -->
209
+      <div class="splitter"
210
+           (mousedown)="startDrag($event)"
211
+           [class.dragging]="isDragging">
212
+        <div class="splitter-handle"></div>
213
+      </div>
214
+      
215
+      <!-- 右侧面板:对话区域 -->
216
+      <div class="right-panel">
217
+        <div class="conversation-wrapper">
218
+          <app-conversation [instanceId]="project?.id"></app-conversation>
219
+        </div>
220
+      </div>
221
+    </div>
222
+  `,
223
+  styles: [`
224
+    .project-tab-container {
225
+      display: grid;
226
+      height: 100vh;
227
+      overflow: hidden;
228
+    }
229
+    
230
+    .left-panel, .right-panel {
231
+      height: 100%;
232
+      overflow-y: auto;
233
+      padding: 16px;
234
+    }
235
+    
236
+    .left-panel {
237
+      background: #f8f9fa;
238
+      display: flex;
239
+      flex-direction: column;
240
+      gap: 16px;
241
+    }
242
+    
243
+    .right-panel {
244
+      background: #ffffff;
245
+    }
246
+    
247
+    .project-info-card {
248
+      margin-bottom: 8px;
249
+    }
250
+    
251
+    .agent-chip {
252
+      display: inline-block;
253
+      background: #e3f2fd;
254
+      color: #1976d2;
255
+      padding: 2px 8px;
256
+      border-radius: 12px;
257
+      font-size: 12px;
258
+      margin-right: 8px;
259
+    }
260
+    
261
+    .status-chip {
262
+      display: inline-block;
263
+      padding: 2px 8px;
264
+      border-radius: 12px;
265
+      font-size: 12px;
266
+    }
267
+    
268
+    .status-requirement_document { background: #fff3e0; color: #f57c00; }
269
+    .status-technical_document { background: #e8f5e8; color: #388e3c; }
270
+    .status-code { background: #e3f2fd; color: #1976d2; }
271
+    .status-test { background: #f3e5f5; color: #7b1fa2; }
272
+    .status-release { background: #e8f5e8; color: #388e3c; }
273
+    
274
+    .project-description {
275
+      color: #666;
276
+      line-height: 1.5;
277
+      margin: 12px 0;
278
+    }
279
+    
280
+    .project-meta {
281
+      display: flex;
282
+      gap: 16px;
283
+      margin: 12px 0;
284
+      color: #888;
285
+      font-size: 14px;
286
+    }
287
+    
288
+    .meta-item {
289
+      display: flex;
290
+      align-items: center;
291
+      gap: 4px;
292
+    }
293
+    
294
+    .meta-item mat-icon {
295
+      font-size: 16px;
296
+      height: 16px;
297
+      width: 16px;
298
+    }
299
+    
300
+    .progress-section {
301
+      margin: 16px 0;
302
+    }
303
+    
304
+    .progress-label {
305
+      font-size: 14px;
306
+      color: #666;
307
+      margin-bottom: 4px;
308
+    }
309
+    
310
+    .progress-text {
311
+      font-size: 12px;
312
+      color: #888;
313
+      text-align: right;
314
+      margin-top: 4px;
315
+    }
316
+    
317
+    .latest-log {
318
+      background: #f5f5f5;
319
+      border-radius: 4px;
320
+      padding: 12px;
321
+      margin-top: 16px;
322
+    }
323
+    
324
+    .log-header {
325
+      display: flex;
326
+      align-items: center;
327
+      gap: 8px;
328
+      font-size: 14px;
329
+      color: #666;
330
+      margin-bottom: 8px;
331
+    }
332
+    
333
+    .log-content {
334
+      font-size: 12px;
335
+      color: #888;
336
+    }
337
+    
338
+    .steps-accordion {
339
+      flex: 1;
340
+      min-height: 0;
341
+      overflow-y: auto;
342
+    }
343
+    
344
+    .step-content {
345
+      padding: 12px 0;
346
+    }
347
+    
348
+    .current-step {
349
+      color: #1976d2;
350
+      font-weight: 500;
351
+    }
352
+    
353
+    .step-status {
354
+      display: inline-block;
355
+      background: #f5f5f5;
356
+      padding: 4px 8px;
357
+      border-radius: 4px;
358
+      font-size: 12px;
359
+      color: #666;
360
+    }
361
+    
362
+    .no-project {
363
+      text-align: center;
364
+      padding: 40px 20px;
365
+      color: #666;
366
+    }
367
+    
368
+    .splitter {
369
+      background: #f0f0f0;
370
+      cursor: col-resize;
371
+      display: flex;
372
+      align-items: center;
373
+      justify-content: center;
374
+      position: relative;
375
+    }
376
+    
377
+    .splitter:hover, .splitter.dragging {
378
+      background: #e0e0e0;
379
+    }
380
+    
381
+    .splitter-handle {
382
+      width: 4px;
383
+      height: 40px;
384
+      background: #ccc;
385
+      border-radius: 2px;
386
+    }
387
+    
388
+    .splitter.dragging .splitter-handle {
389
+      background: #007bff;
390
+    }
391
+    
392
+    .conversation-wrapper {
393
+      height: 100%;
394
+      overflow: hidden;
395
+    }
396
+  `]
397
+})
398
+export class ProjectTabComponent implements OnInit, OnDestroy {
399
+  @Input() tabId?: string;
400
+  
401
+  project: Session | null = null;
402
+  activeTab: Tab | null = null;
403
+  
404
+  leftWidth = 320;
405
+  isDragging = false;
406
+  minLeftWidth = 280;
407
+  maxLeftWidth = 500;
408
+  
409
+  private subscriptions: Subscription = new Subscription();
410
+  
411
+  constructor(
412
+    private tabService: TabService,
413
+    private sessionService: SessionService,
414
+    private agentService: AgentService
415
+  ) {}
416
+  
417
+  ngOnInit() {
418
+    // 从本地存储恢复宽度
419
+    const savedWidth = localStorage.getItem('project_tab_leftWidth');
420
+    if (savedWidth) {
421
+      this.leftWidth = parseInt(savedWidth, 10);
422
+    }
423
+    
424
+    // 获取活动页签
425
+    this.subscriptions.add(
426
+      this.tabService.activeTab$.subscribe(tab => {
427
+        this.activeTab = tab || null;
428
+        if (tab) {
429
+          this.project = tab.session;
430
+          // 设置活动会话
431
+          this.sessionService.setActiveSession(this.project);
432
+        } else {
433
+          this.project = null;
434
+        }
435
+      })
436
+    );
437
+    
438
+    // 如果指定了tabId,直接获取
439
+    if (this.tabId) {
440
+      const tab = this.tabService.getTabById(this.tabId);
441
+      if (tab) {
442
+        this.activeTab = tab;
443
+        this.project = tab.session;
444
+        this.sessionService.setActiveSession(this.project);
445
+      }
446
+    }
447
+  }
448
+  
449
+  getAgentDisplayName(agentId: string): string {
450
+    return this.agentService.getAgentDisplayName(agentId);
451
+  }
452
+  
453
+  getStatusDisplayName(status: string): string {
454
+    const statusMap: Record<string, string> = {
455
+      'requirement_document': '需求文档',
456
+      'technical_document': '技术文档',
457
+      'code': '代码开发',
458
+      'test': '测试',
459
+      'release': '发布'
460
+    };
461
+    return statusMap[status] || status;
462
+  }
463
+  
464
+  getStatusClass(status: string): string {
465
+    return `status-${status}`;
466
+  }
467
+  
468
+  formatDate(dateString?: string): string {
469
+    if (!dateString) return '未知';
470
+    const date = new Date(dateString);
471
+    return date.toLocaleDateString('zh-CN');
472
+  }
473
+  
474
+  getProgressValue(status: string): number {
475
+    const progressMap: Record<string, number> = {
476
+      'requirement_document': 20,
477
+      'technical_document': 40,
478
+      'code': 60,
479
+      'test': 80,
480
+      'release': 100
481
+    };
482
+    return progressMap[status] || 0;
483
+  }
484
+  
485
+  getProgressText(status: string): string {
486
+    const progressMap: Record<string, string> = {
487
+      'requirement_document': '20% - 需求分析',
488
+      'technical_document': '40% - 技术设计',
489
+      'code': '60% - 代码开发',
490
+      'test': '80% - 测试验证',
491
+      'release': '100% - 发布完成'
492
+    };
493
+    return progressMap[status] || '0% - 未开始';
494
+  }
495
+  
496
+  focusRequirementInput() {
497
+    // TODO: 聚焦到对话输入框,并提示输入需求
498
+    console.log('聚焦需求输入');
499
+    // 可以通过服务或事件通知ConversationComponent
500
+  }
501
+  
502
+  startDrag(event: MouseEvent) {
503
+    event.preventDefault();
504
+    this.isDragging = true;
505
+    document.body.style.cursor = 'col-resize';
506
+    document.body.style.userSelect = 'none';
507
+  }
508
+  
509
+  @HostListener('document:mousemove', ['$event'])
510
+  onDrag(event: MouseEvent) {
511
+    if (this.isDragging) {
512
+      const newWidth = event.clientX;
513
+      if (newWidth >= this.minLeftWidth && newWidth <= this.maxLeftWidth) {
514
+        this.leftWidth = newWidth;
515
+      }
516
+    }
517
+  }
518
+  
519
+  @HostListener('document:mouseup')
520
+  stopDrag() {
521
+    if (this.isDragging) {
522
+      this.isDragging = false;
523
+      localStorage.setItem('project_tab_leftWidth', this.leftWidth.toString());
524
+    }
525
+    document.body.style.cursor = '';
526
+    document.body.style.userSelect = '';
527
+  }
528
+  
529
+  ngOnDestroy() {
530
+    this.subscriptions.unsubscribe();
531
+  }
532
+}

+ 9
- 0
src/app/models/event.model.ts Zobrazit soubor

@@ -35,6 +35,15 @@ export interface SessionInfo {
35 35
     deletions: number;
36 36
     files: number;
37 37
   };
38
+  // 扩展字段(用于项目功能)
39
+  project_id?: string;
40
+  agent_name?: string;
41
+  description?: string;
42
+  status?: string;
43
+  user_id?: string;
44
+  tenant_id?: string;
45
+  created_at?: string;
46
+  updated_at?: string;
38 47
 }
39 48
 
40 49
 // 会话差异事件

+ 103
- 20
src/app/models/session.model.ts Zobrazit soubor

@@ -1,36 +1,119 @@
1
-// 会话信息
1
+// 会话状态枚举
2
+export enum SessionStatus {
3
+  RequirementDocument = 'requirement_document',
4
+  TechnicalDocument = 'technical_document',
5
+  Code = 'code',
6
+  Test = 'test',
7
+  Release = 'release'
8
+}
9
+
10
+// 智能体枚举
11
+export enum Agent {
12
+  Replenish = 'replenish',
13
+  Transfer = 'transfer',
14
+  Allocation = 'allocation',
15
+  Report = 'report'
16
+}
17
+
18
+// 智能体显示名称映射
19
+export const AgentDisplayName: Record<Agent, string> = {
20
+  [Agent.Replenish]: '补货',
21
+  [Agent.Transfer]: '调拨',
22
+  [Agent.Allocation]: '配货',
23
+  [Agent.Report]: '报表'
24
+}
25
+
26
+// 会话信息(对应后端Session模型)
2 27
 export interface Session {
3
-  id: string;
4
-  title: string;
28
+  id: string;                    // 会话ID
29
+  project_id: string;           // 项目ID
30
+  title: string;                // 标题
31
+  agent_name: string;           // 智能体名称
32
+  description?: string;         // 项目描述
33
+  status: string;               // 状态:requirement_document|technical_document|code|test|release
34
+  user_id?: string;             // 用户ID
35
+  tenant_id?: string;           // 租户ID
36
+  created_at?: string;          // 创建时间
37
+  updated_at?: string;          // 更新时间
38
+  
39
+  // 向后兼容字段
5 40
   parentID?: string;
6 41
   path?: { [key: string]: string };
7
-  createdAt?: string;
8 42
   port?: number;
9 43
   baseURL?: string;
10 44
 }
11 45
 
12
-// 创建会话请求
13
-export interface SessionCreateRequest {
14
-  title: string;
15
-  menu_item_id: string; // 菜单项ID,必填
46
+// 代码项(对应后端CodeItem模型)
47
+export interface CodeItem {
48
+  order: number;                // 执行次序
49
+  title: string;                // 步骤标题
50
+  select_part: string;          // select部分SQL代码
51
+  from_part: string;            // from部分代码
52
+  where_part: string;           // where部分
53
+  group_by_part: string;        // group by部分
54
+  order_by_part: string;        // order by部分
55
+  temp_table_name: string;      // 临时表名称
56
+  parameters: Record<string, any>; // 参数集合
57
+  return_columns: Record<string, string>; // 返回的列名称和对应的中文名称(映射)
58
+}
59
+
60
+// 会话明细(对应后端SessionDetail模型)
61
+export interface SessionDetail {
62
+  id?: string;                  // MongoDB自动生成的ID
63
+  session_id: string;           // 关联的会话ID
64
+  requirement_doc: string;      // 需求文档
65
+  technical_doc: string;        // 技术文档
66
+  code_items: CodeItem[];       // 代码数组
67
+  history_sessions: string[];   // 历史会话ID数组
68
+  created_at?: string;          // 创建时间
69
+  updated_at?: string;          // 更新时间
70
+}
71
+
72
+// 包含主会话和明细的完整响应
73
+export interface SessionWithDetail {
74
+  session: Session;
75
+  detail?: SessionDetail;
76
+  history_count?: number;       // 历史消息数量
77
+}
78
+
79
+// 创建项目请求(对应后端SessionCreateRequest)
80
+export interface ProjectCreateRequest {
81
+  title: string;                // 标题
82
+  agent_name: string;           // 智能体名称
83
+  project_id?: string;          // 项目ID(前端生成proj_前缀UUID)
84
+  description?: string;         // 项目描述
85
+}
86
+
87
+// 创建项目响应
88
+export interface ProjectCreateResponse extends Session {
89
+  // 继承Session所有字段
90
+}
91
+
92
+// 创建项目API响应
93
+export interface ProjectCreateApiResponse extends QueryResult<ProjectCreateResponse> {
94
+  // data字段包含ProjectCreateResponse
95
+}
96
+
97
+// 项目列表响应
98
+export interface ProjectListResponse extends QueryResult<Session[]> {
99
+  // data字段包含会话数组(按项目分组)
100
+}
101
+
102
+// 向后兼容:保留原有接口名称
103
+export interface SessionCreateRequest extends ProjectCreateRequest {
104
+  // 保持兼容,实际使用ProjectCreateRequest
16 105
 }
17 106
 
18
-// 创建会话响应
19
-export interface SessionCreateResponse {
20
-  id: string;
21
-  title: string;
22
-  port: number;
23
-  baseURL: string;
107
+export interface SessionCreateResponse extends ProjectCreateResponse {
108
+  // 保持兼容
24 109
 }
25 110
 
26
-// 创建会话响应
27
-export interface SessionCreateApiResponse extends QueryResult<SessionCreateResponse> {
28
-  // data字段包含SessionCreateResponse
111
+export interface SessionCreateApiResponse extends ProjectCreateApiResponse {
112
+  // 保持兼容
29 113
 }
30 114
 
31
-// 会话列表响应
32
-export interface SessionListResponse extends QueryResult<Session[]> {
33
-  // data字段包含会话数组
115
+export interface SessionListResponse extends ProjectListResponse {
116
+  // 保持兼容
34 117
 }
35 118
 
36 119
 // 从通用QueryResult导入

+ 82
- 0
src/app/services/agent.service.ts Zobrazit soubor

@@ -0,0 +1,82 @@
1
+import { Injectable } from '@angular/core';
2
+import { HttpClient } from '@angular/common/http';
3
+import { Observable } from 'rxjs';
4
+import { map } from 'rxjs/operators';
5
+import { Agent, AgentDisplayName } from '../models/session.model';
6
+
7
+// 智能体项接口(对应后端AgentItem)
8
+export interface AgentItem {
9
+  id: string;   // 智能体ID
10
+  name: string; // 智能体名称(标题)
11
+}
12
+
13
+// 智能体列表响应
14
+interface AgentListResponse {
15
+  success: boolean;
16
+  message?: string;
17
+  data?: AgentItem[];
18
+}
19
+
20
+@Injectable({
21
+  providedIn: 'root'
22
+})
23
+export class AgentService {
24
+  
25
+  constructor(private http: HttpClient) {}
26
+
27
+  /**
28
+   * 获取智能体列表
29
+   */
30
+  getAgents(): Observable<AgentItem[]> {
31
+    return this.http.get<AgentListResponse>('/api/agents').pipe(
32
+      map(response => {
33
+        if (response.success && response.data) {
34
+          return response.data;
35
+        } else {
36
+          throw new Error(response.message || '获取智能体列表失败');
37
+        }
38
+      })
39
+    );
40
+  }
41
+
42
+  /**
43
+   * 获取智能体显示名称
44
+   * @param agentId 智能体ID
45
+   */
46
+  getAgentDisplayName(agentId: string): string {
47
+    // 首先尝试从映射中获取
48
+    if (agentId in AgentDisplayName) {
49
+      return AgentDisplayName[agentId as Agent];
50
+    }
51
+    
52
+    // 如果映射中没有,返回ID本身
53
+    return agentId;
54
+  }
55
+
56
+  /**
57
+   * 获取智能体选项列表(用于下拉框)
58
+   */
59
+  getAgentOptions(): Observable<{value: string, label: string}[]> {
60
+    return this.getAgents().pipe(
61
+      map(agents => agents.map(agent => ({
62
+        value: agent.id,
63
+        label: this.getAgentDisplayName(agent.id)
64
+      })))
65
+    );
66
+  }
67
+
68
+  /**
69
+   * 验证智能体ID是否有效
70
+   */
71
+  isValidAgent(agentId: string): boolean {
72
+    return agentId in AgentDisplayName;
73
+  }
74
+
75
+  /**
76
+   * 获取默认智能体(第一个智能体)
77
+   */
78
+  getDefaultAgent(): string {
79
+    const agents = Object.keys(AgentDisplayName);
80
+    return agents.length > 0 ? agents[0] : 'report';
81
+  }
82
+}

+ 76
- 17
src/app/services/session.service.ts Zobrazit soubor

@@ -2,7 +2,14 @@ import { Injectable, OnDestroy, Injector } from '@angular/core';
2 2
 import { HttpClient } from '@angular/common/http';
3 3
 import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
4 4
 import { map, tap } from 'rxjs/operators';
5
-import { Session, SessionCreateRequest, SessionCreateApiResponse, SessionListResponse } from '../models/session.model';
5
+import { 
6
+  Session, 
7
+  SessionCreateRequest, 
8
+  SessionCreateApiResponse, 
9
+  SessionListResponse,
10
+  ProjectCreateRequest,
11
+  ProjectCreateApiResponse
12
+} from '../models/session.model';
6 13
 import { AuthService } from './auth.service';
7 14
 import { EventService } from './event.service';
8 15
 import { SessionUpdatedEvent } from '../models/event.model';
@@ -45,7 +52,7 @@ export class SessionService implements OnDestroy {
45 52
     return this._eventService;
46 53
   }
47 54
 
48
-  // 获取会话列表(需要认证)
55
+  // 获取会话列表(需要认证)- 使用新API端点
49 56
   getSessions(): Observable<Session[]> {
50 57
     // 检查用户是否已认证
51 58
     if (!this.authService.isAuthenticated()) {
@@ -53,13 +60,39 @@ export class SessionService implements OnDestroy {
53 60
       return of([]);
54 61
     }
55 62
     
56
-    return this.http.get<SessionListResponse>('/api/session/list').pipe(
63
+    return this.http.get<any>('/api/sessions').pipe(
57 64
       map(response => {
65
+        console.log('🔍 [SessionService] 获取会话列表响应:', response);
66
+        
58 67
         if (response.success && response.data) {
59
-          this.sessions = response.data;
68
+          // 处理嵌套的 sessions 字段(后端返回 { data: { sessions: [], ...pagination } })
69
+          let sessionsArray: any[] = [];
70
+          
71
+          if (response.data.sessions && Array.isArray(response.data.sessions)) {
72
+            // 新格式:response.data.sessions 是数组
73
+            sessionsArray = response.data.sessions;
74
+            console.log('🔍 [SessionService] 从嵌套 sessions 字段获取数据,数量:', sessionsArray.length);
75
+          } else if (Array.isArray(response.data)) {
76
+            // 兼容旧格式:response.data 直接是数组
77
+            sessionsArray = response.data;
78
+            console.log('🔍 [SessionService] 从 data 字段直接获取数组,数量:', sessionsArray.length);
79
+          } else {
80
+            console.warn('🔍 [SessionService] 无法识别响应数据格式,使用空数组');
81
+            sessionsArray = [];
82
+          }
83
+          
84
+          // 确保每个会话都有 project_id 字段(向后兼容)
85
+          const sessions = sessionsArray.map(session => ({
86
+            ...session,
87
+            project_id: session.project_id || session.id // 如果没有 project_id,使用 id 作为默认
88
+          }));
89
+          
90
+          this.sessions = sessions;
60 91
           this.sessionsSubject.next(this.sessions);
61
-          return response.data;
92
+          console.log('🔍 [SessionService] 会话列表处理完成,数量:', sessions.length);
93
+          return sessions;
62 94
         } else {
95
+          console.error('🔍 [SessionService] 获取会话列表失败:', response.message);
63 96
           throw new Error(response.message || '获取会话列表失败');
64 97
         }
65 98
       }),
@@ -72,26 +105,41 @@ export class SessionService implements OnDestroy {
72 105
     );
73 106
   }
74 107
 
75
-  // 创建新会话
108
+  // 创建新会话(向后兼容,建议使用createProject)
76 109
   createSession(title: string, menuItemId: string): Observable<Session> {
110
+    console.warn('createSession已过时,请使用createProject方法');
111
+    // 尝试使用默认智能体创建项目
112
+    const defaultAgent = 'report'; // 默认智能体
113
+    return this.createProject({
114
+      title,
115
+      agent_name: defaultAgent,
116
+      description: `由旧会话创建,菜单项: ${menuItemId}`
117
+    });
118
+  }
119
+
120
+  // 创建新项目
121
+  createProject(request: ProjectCreateRequest): Observable<Session> {
77 122
     // 检查用户是否已认证
78 123
     if (!this.authService.isAuthenticated()) {
79
-      console.error('用户未认证,无法创建会话');
124
+      console.error('用户未认证,无法创建项目');
80 125
       throw new Error('用户未认证,请先登录');
81 126
     }
82 127
     
83
-    const request: SessionCreateRequest = { title, menu_item_id: menuItemId };
128
+    // 生成项目ID(如果未提供)
129
+    const projectRequest = {
130
+      ...request,
131
+      project_id: request.project_id || `proj_${crypto.randomUUID()}`
132
+    };
84 133
     
85
-    return this.http.post<SessionCreateApiResponse>('/api/session/create', request).pipe(
134
+    return this.http.post<ProjectCreateApiResponse>('/api/session/create', projectRequest).pipe(
86 135
       map(response => {
87
-        console.log('🔍 [SessionService] 创建会话响应:', response);
136
+        console.log('🔍 [SessionService] 创建项目响应:', response);
88 137
         if (response.success && response.data) {
89 138
           const sessionData = response.data;
139
+          // 确保有project_id字段
90 140
           const newSession: Session = {
91
-            id: sessionData.id,
92
-            title: sessionData.title,
93
-            port: sessionData.port,
94
-            baseURL: sessionData.baseURL
141
+            ...sessionData,
142
+            project_id: sessionData.project_id || projectRequest.project_id
95 143
           };
96 144
           
97 145
           this.sessions.unshift(newSession);
@@ -100,7 +148,7 @@ export class SessionService implements OnDestroy {
100 148
           
101 149
           return newSession;
102 150
         } else {
103
-          throw new Error(response.message || '创建会话失败');
151
+          throw new Error(response.message || '创建项目失败');
104 152
         }
105 153
       })
106 154
     );
@@ -129,9 +177,13 @@ export class SessionService implements OnDestroy {
129 177
       return;
130 178
     }
131 179
     
180
+    console.log('🔍 [SessionService] 开始加载会话列表...');
132 181
     this.getSessions().subscribe({
182
+      next: (sessions) => {
183
+        console.log('🔍 [SessionService] 会话列表加载成功,数量:', sessions.length);
184
+      },
133 185
       error: (error) => {
134
-        console.error('加载会话列表失败:', error);
186
+        console.error('🔍 [SessionService] 加载会话列表失败:', error);
135 187
         // 如果API不可用,使用空列表
136 188
         this.sessionsSubject.next([]);
137 189
       }
@@ -183,7 +235,10 @@ export class SessionService implements OnDestroy {
183 235
       const updatedSession: Session = {
184 236
         ...this.sessions[existingSessionIndex],
185 237
         title: sessionInfo.title,
186
-        // 保留其他字段如port、baseURL等
238
+        agent_name: sessionInfo.agent_name || this.sessions[existingSessionIndex].agent_name,
239
+        description: sessionInfo.description || this.sessions[existingSessionIndex].description,
240
+        status: sessionInfo.status || this.sessions[existingSessionIndex].status,
241
+        // 保留其他字段
187 242
       };
188 243
       this.sessions[existingSessionIndex] = updatedSession;
189 244
       console.log('SessionService: 更新现有会话', updatedSession.title);
@@ -191,7 +246,11 @@ export class SessionService implements OnDestroy {
191 246
       // 添加新会话到列表开头
192 247
       const newSession: Session = {
193 248
         id: sessionInfo.id,
249
+        project_id: sessionInfo.project_id || sessionInfo.projectID || sessionInfo.id,
194 250
         title: sessionInfo.title,
251
+        agent_name: sessionInfo.agent_name || 'report',
252
+        description: sessionInfo.description || '',
253
+        status: sessionInfo.status || 'requirement_document',
195 254
         // port和baseURL可能在后端创建会话时提供,这里使用默认值
196 255
         // 实际使用中,可以从事件或后续API调用中获取
197 256
         port: 8020, // 默认端口,可根据需要调整

+ 283
- 0
src/app/services/tab.service.ts Zobrazit soubor

@@ -0,0 +1,283 @@
1
+import { Injectable } from '@angular/core';
2
+import { BehaviorSubject, Observable } from 'rxjs';
3
+import { Session } from '../models/session.model';
4
+
5
+// 页签接口
6
+export interface Tab {
7
+  id: string;           // 页签ID(使用project_id)
8
+  projectId: string;    // 项目ID
9
+  title: string;        // 页签标题(使用项目标题)
10
+  session: Session;     // 项目会话信息
11
+  createdAt: Date;      // 打开时间
12
+}
13
+
14
+// 页签状态
15
+export interface TabState {
16
+  tabs: Tab[];
17
+  activeTabId: string | null;
18
+}
19
+
20
+@Injectable({
21
+  providedIn: 'root'
22
+})
23
+export class TabService {
24
+  private readonly STORAGE_KEY = 'project_tabs_state';
25
+  private readonly MAX_TABS = 7;
26
+  
27
+  private tabsSubject = new BehaviorSubject<Tab[]>([]);
28
+  private activeTabIdSubject = new BehaviorSubject<string | null>(null);
29
+  
30
+  tabs$ = this.tabsSubject.asObservable();
31
+  activeTabId$ = this.activeTabIdSubject.asObservable();
32
+  activeTab$ = this.activeTabId$.pipe(
33
+    map(activeId => activeId ? this.tabsSubject.value.find(tab => tab.id === activeId) : undefined)
34
+  );
35
+  
36
+  constructor() {
37
+    this.loadFromStorage();
38
+  }
39
+
40
+  /**
41
+   * 打开项目页签
42
+   * @param session 项目会话
43
+   * @returns 是否成功打开
44
+   */
45
+  openProjectTab(session: Session): boolean {
46
+    const projectId = session.project_id;
47
+    
48
+    // 检查是否已打开
49
+    const existingTab = this.tabsSubject.value.find(tab => tab.projectId === projectId);
50
+    if (existingTab) {
51
+      // 已打开,激活该页签
52
+      this.activateTab(existingTab.id);
53
+      return true;
54
+    }
55
+    
56
+    // 检查页签数量限制
57
+    if (this.tabsSubject.value.length >= this.MAX_TABS) {
58
+      alert(`最多只能打开 ${this.MAX_TABS} 个页签,请先关闭一个页签`);
59
+      return false;
60
+    }
61
+    
62
+    // 创建新页签
63
+    const tab: Tab = {
64
+      id: projectId,  // 使用project_id作为页签ID
65
+      projectId: projectId,
66
+      title: session.title,
67
+      session: session,
68
+      createdAt: new Date()
69
+    };
70
+    
71
+    // 添加页签并激活
72
+    const tabs = [...this.tabsSubject.value, tab];
73
+    this.tabsSubject.next(tabs);
74
+    this.activateTab(tab.id);
75
+    this.saveToStorage();
76
+    
77
+    return true;
78
+  }
79
+  
80
+  /**
81
+   * 关闭页签
82
+   * @param tabId 页签ID
83
+   */
84
+  closeTab(tabId: string): void {
85
+    const tabs = this.tabsSubject.value.filter(tab => tab.id !== tabId);
86
+    this.tabsSubject.next(tabs);
87
+    
88
+    // 如果关闭的是活动页签,设置新的活动页签
89
+    if (this.activeTabIdSubject.value === tabId) {
90
+      this.activeTabIdSubject.next(tabs.length > 0 ? tabs[0].id : null);
91
+    }
92
+    
93
+    this.saveToStorage();
94
+  }
95
+  
96
+  /**
97
+   * 激活页签
98
+   * @param tabId 页签ID
99
+   */
100
+  activateTab(tabId: string): void {
101
+    const tab = this.tabsSubject.value.find(t => t.id === tabId);
102
+    if (tab) {
103
+      this.activeTabIdSubject.next(tabId);
104
+      this.saveToStorage();
105
+    }
106
+  }
107
+  
108
+  /**
109
+   * 设置活动页签(支持null)
110
+   * @param tabId 页签ID或null
111
+   */
112
+  setActiveTab(tabId: string | null): void {
113
+    this.activeTabIdSubject.next(tabId);
114
+    this.saveToStorage();
115
+  }
116
+  
117
+  /**
118
+   * 关闭所有页签
119
+   */
120
+  closeAllTabs(): void {
121
+    this.tabsSubject.next([]);
122
+    this.activeTabIdSubject.next(null);
123
+    this.saveToStorage();
124
+  }
125
+  
126
+  /**
127
+   * 关闭其他页签(保留指定页签)
128
+   * @param tabId 要保留的页签ID
129
+   */
130
+  closeOtherTabs(tabId: string): void {
131
+    const tab = this.tabsSubject.value.find(t => t.id === tabId);
132
+    if (tab) {
133
+      this.tabsSubject.next([tab]);
134
+      this.activeTabIdSubject.next(tabId);
135
+      this.saveToStorage();
136
+    }
137
+  }
138
+  
139
+  /**
140
+   * 关闭左侧页签
141
+   * @param tabId 当前页签ID
142
+   */
143
+  closeLeftTabs(tabId: string): void {
144
+    const tabs = this.tabsSubject.value;
145
+    const tabIndex = tabs.findIndex(t => t.id === tabId);
146
+    
147
+    if (tabIndex > 0) {
148
+      const remainingTabs = tabs.slice(tabIndex);
149
+      this.tabsSubject.next(remainingTabs);
150
+      
151
+      // 如果活动页签被关闭,激活当前页签
152
+      const activeTabId = this.activeTabIdSubject.value;
153
+      if (activeTabId && !remainingTabs.some(t => t.id === activeTabId)) {
154
+        this.activeTabIdSubject.next(tabId);
155
+      }
156
+      
157
+      this.saveToStorage();
158
+    }
159
+  }
160
+  
161
+  /**
162
+   * 关闭右侧页签
163
+   * @param tabId 当前页签ID
164
+   */
165
+  closeRightTabs(tabId: string): void {
166
+    const tabs = this.tabsSubject.value;
167
+    const tabIndex = tabs.findIndex(t => t.id === tabId);
168
+    
169
+    if (tabIndex >= 0 && tabIndex < tabs.length - 1) {
170
+      const remainingTabs = tabs.slice(0, tabIndex + 1);
171
+      this.tabsSubject.next(remainingTabs);
172
+      
173
+      // 如果活动页签被关闭,激活当前页签
174
+      const activeTabId = this.activeTabIdSubject.value;
175
+      if (activeTabId && !remainingTabs.some(t => t.id === activeTabId)) {
176
+        this.activeTabIdSubject.next(tabId);
177
+      }
178
+      
179
+      this.saveToStorage();
180
+    }
181
+  }
182
+  
183
+  /**
184
+   * 获取页签数量
185
+   */
186
+  getTabCount(): number {
187
+    return this.tabsSubject.value.length;
188
+  }
189
+  
190
+  /**
191
+   * 获取页签是否已满
192
+   */
193
+  isTabFull(): boolean {
194
+    return this.tabsSubject.value.length >= this.MAX_TABS;
195
+  }
196
+  
197
+  /**
198
+   * 根据项目ID获取页签
199
+   */
200
+  getTabByProjectId(projectId: string): Tab | undefined {
201
+    return this.tabsSubject.value.find(tab => tab.projectId === projectId);
202
+  }
203
+  
204
+  /**
205
+   * 获取当前活动页签
206
+   */
207
+  getActiveTab(): Tab | undefined {
208
+    const activeId = this.activeTabIdSubject.value;
209
+    return activeId ? this.getTabById(activeId) : undefined;
210
+  }
211
+  
212
+  /**
213
+   * 根据ID获取页签
214
+   */
215
+  getTabById(tabId: string): Tab | undefined {
216
+    return this.tabsSubject.value.find(tab => tab.id === tabId);
217
+  }
218
+  
219
+  /**
220
+   * 获取所有页签
221
+   */
222
+  getTabs(): Tab[] {
223
+    return this.tabsSubject.value;
224
+  }
225
+  
226
+  /**
227
+   * 更新页签会话信息(当会话更新时调用)
228
+   */
229
+  updateTabSession(projectId: string, session: Session): void {
230
+    const tabs = this.tabsSubject.value.map(tab => {
231
+      if (tab.projectId === projectId) {
232
+        return {
233
+          ...tab,
234
+          title: session.title,
235
+          session: session
236
+        };
237
+      }
238
+      return tab;
239
+    });
240
+    
241
+    this.tabsSubject.next(tabs);
242
+    this.saveToStorage();
243
+  }
244
+  
245
+  // 私有方法:存储管理
246
+  private loadFromStorage(): void {
247
+    try {
248
+      const saved = localStorage.getItem(this.STORAGE_KEY);
249
+      if (saved) {
250
+        const state: TabState = JSON.parse(saved);
251
+        // 转换日期字符串为Date对象
252
+        const tabs = state.tabs.map(tab => ({
253
+          ...tab,
254
+          createdAt: new Date(tab.createdAt),
255
+          session: {
256
+            ...tab.session,
257
+            // 确保session有必要的字段
258
+            project_id: tab.session.project_id || tab.session.id
259
+          }
260
+        }));
261
+        this.tabsSubject.next(tabs);
262
+        this.activeTabIdSubject.next(state.activeTabId);
263
+      }
264
+    } catch (error) {
265
+      console.error('加载页签状态失败:', error);
266
+    }
267
+  }
268
+  
269
+  private saveToStorage(): void {
270
+    try {
271
+      const state: TabState = {
272
+        tabs: this.tabsSubject.value,
273
+        activeTabId: this.activeTabIdSubject.value
274
+      };
275
+      localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
276
+    } catch (error) {
277
+      console.error('保存页签状态失败:', error);
278
+    }
279
+  }
280
+}
281
+
282
+// 导入rxjs操作符
283
+import { map } from 'rxjs/operators';

Loading…
Zrušit
Uložit