Browse Source

Release v0.2.21502

qdy 2 weeks ago
commit
82f2bbb314

+ 1
- 27
src/app/modules/editor/editor.component.html View File

@@ -56,33 +56,7 @@
56 56
       <!-- 消息容器 -->
57 57
       <div class="messages-container" #messagesContainer>
58 58
         @for (message of messages; track message.id) {
59
-          <div class="message-wrapper" [class.user-message]="message.role === 'user'" [class.ai-message]="message.role === 'assistant'">
60
-            <div class="message-bubble">
61
-              @if (message.role === 'user') {
62
-                <div class="message-header">
63
-                  <mat-icon class="message-avatar">person</mat-icon>
64
-                  <span class="message-role">用户</span>
65
-                  <span class="message-time">{{ message.timestamp | date:'HH:mm' }}</span>
66
-                </div>
67
-              } @else {
68
-                <div class="message-header">
69
-                  <mat-icon class="message-avatar">smart_toy</mat-icon>
70
-                  <span class="message-role">AI助手</span>
71
-                  <span class="message-time">{{ message.timestamp | date:'HH:mm' }}</span>
72
-                </div>
73
-              }
74
-              
75
-               <div class="message-content">
76
-                 @if (message.loading) {
77
-                   <div class="loading-indicator">
78
-                     <mat-spinner diameter="16"></mat-spinner>
79
-                     <span>思考中...</span>
80
-                   </div>
81
-                 }
82
-                 <div [innerHTML]="message.content | markdown"></div>
83
-               </div>
84
-            </div>
85
-          </div>
59
+          <app-chat-message [message]="message"></app-chat-message>
86 60
         }
87 61
         
88 62
         @if (messages.length === 0) {

+ 31
- 323
src/app/modules/editor/editor.component.scss View File

@@ -5,48 +5,44 @@
5 5
   background-color: #f5f5f5;
6 6
 }
7 7
 
8
-.editor-toolbar {
9
-  flex-shrink: 0;
8
+.toolbar-title {
9
+  font-size: 18px;
10
+  font-weight: 500;
11
+  margin-left: 12px;
10 12
   
11
-  .toolbar-title {
12
-    font-size: 18px;
13
-    font-weight: 500;
14
-    margin-left: 12px;
15
-    
16
-    .project-subtitle {
17
-      margin-left: 8px;
18
-      font-size: 14px;
19
-      opacity: 0.8;
20
-      font-weight: normal;
21
-    }
13
+  .project-subtitle {
14
+    margin-left: 8px;
15
+    font-size: 14px;
16
+    opacity: 0.8;
17
+    font-weight: normal;
22 18
   }
19
+}
20
+
21
+.spacer {
22
+  flex: 1 1 auto;
23
+}
24
+
25
+.toolbar-spinner {
26
+  margin-right: 8px;
27
+}
28
+
29
+.sse-status {
30
+  display: flex;
31
+  align-items: center;
32
+  gap: 4px;
33
+  margin-right: 8px;
34
+  font-size: 12px;
23 35
   
24
-  .spacer {
25
-    flex: 1 1 auto;
36
+  &.connected {
37
+    color: #4caf50;
26 38
   }
27 39
   
28
-  .toolbar-spinner {
29
-    margin-right: 8px;
40
+  &.disconnected {
41
+    color: #f44336;
30 42
   }
31 43
   
32
-  .sse-status {
33
-    display: flex;
34
-    align-items: center;
35
-    gap: 4px;
36
-    margin-right: 8px;
37
-    font-size: 12px;
38
-    
39
-    &.connected {
40
-      color: #4caf50;
41
-    }
42
-    
43
-    &.disconnected {
44
-      color: #f44336;
45
-    }
46
-    
47
-    .sse-text {
48
-      margin-left: 4px;
49
-    }
44
+  .sse-text {
45
+    margin-left: 4px;
50 46
   }
51 47
 }
52 48
 
@@ -159,87 +155,7 @@
159 155
   }
160 156
 }
161 157
 
162
-.message-wrapper {
163
-  display: flex;
164
-  max-width: 80%;
165
-  
166
-  &.user-message {
167
-    align-self: flex-end;
168
-    
169
-    .message-bubble {
170
-      background-color: #1976d2;
171
-      color: white;
172
-      border-radius: 18px 18px 4px 18px;
173
-    }
174
-    
175
-    .message-header {
176
-      color: rgba(255, 255, 255, 0.8);
177
-    }
178
-  }
179
-  
180
-  &.ai-message {
181
-    align-self: flex-start;
182
-    
183
-    .message-bubble {
184
-      background-color: white;
185
-      color: #333;
186
-      border-radius: 18px 18px 18px 4px;
187
-      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
188
-    }
189
-    
190
-    .message-header {
191
-      color: #666;
192
-    }
193
-  }
194
-}
195
-
196
-.message-bubble {
197
-  padding: 12px 16px;
198
-  word-wrap: break-word;
199
-  word-break: break-word;
200
-  line-height: 1.5;
201
-}
202
-
203
-.message-header {
204
-  display: flex;
205
-  align-items: center;
206
-  gap: 8px;
207
-  margin-bottom: 8px;
208
-  font-size: 12px;
209
-  
210
-  .message-avatar {
211
-    font-size: 16px;
212
-    height: 16px;
213
-    width: 16px;
214
-  }
215
-  
216
-  .message-role {
217
-    font-weight: 500;
218
-    flex: 1;
219
-  }
220
-  
221
-  .message-time {
222
-    opacity: 0.7;
223
-  }
224
-}
225 158
 
226
-.message-content {
227
-  font-size: 14px;
228
-  line-height: 1.5;
229
-  white-space: pre-wrap;
230
-  
231
-  .loading-indicator {
232
-    display: flex;
233
-    align-items: center;
234
-    gap: 8px;
235
-    margin-bottom: 8px;
236
-    color: #666;
237
-    
238
-    span {
239
-      font-size: 12px;
240
-    }
241
-  }
242
-}
243 159
 
244 160
 .input-container {
245 161
   flex-shrink: 0;
@@ -311,211 +227,3 @@
311 227
   background: #a8a8a8;
312 228
 }
313 229
 
314
-/* Markdown渲染样式 */
315
-.message-content {
316
-  /* 可折叠标签样式 */
317
-  ::ng-deep details {
318
-    margin: 8px 0;
319
-    border-radius: 8px;
320
-    overflow: hidden;
321
-  }
322
-
323
-  ::ng-deep .thinking-details {
324
-    border: 2px solid #2196f3;
325
-    background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
326
-    border-radius: 8px;
327
-    box-shadow: 0 2px 8px rgba(33, 150, 243, 0.2);
328
-  }
329
-
330
-  ::ng-deep .tool-details {
331
-    border: 2px solid #9c27b0;
332
-    background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%);
333
-    border-radius: 8px;
334
-    box-shadow: 0 2px 8px rgba(156, 39, 176, 0.2);
335
-  }
336
-
337
-  ::ng-deep .thinking-summary,
338
-  ::ng-deep .tool-summary {
339
-    padding: 8px 12px;
340
-    cursor: pointer;
341
-    font-weight: 500;
342
-    display: flex;
343
-    align-items: center;
344
-    gap: 6px;
345
-    user-select: none;
346
-  }
347
-
348
-  ::ng-deep .thinking-summary {
349
-    color: #0d47a1;
350
-    background: linear-gradient(135deg, #bbdefb 0%, #90caf9 100%);
351
-    border-bottom: 1px solid #64b5f6;
352
-    font-weight: 600;
353
-  }
354
-
355
-  ::ng-deep .tool-summary {
356
-    color: #4a148c;
357
-    background: linear-gradient(135deg, #e1bee7 0%, #ce93d8 100%);
358
-    border-bottom: 1px solid #ba68c8;
359
-    font-weight: 600;
360
-  }
361
-
362
-  /* details元素内的内容样式 */
363
-  ::ng-deep .thinking-details > *:not(summary),
364
-  ::ng-deep .tool-details > *:not(summary) {
365
-    padding: 12px;
366
-    border-top: 1px solid rgba(0, 0, 0, 0.05);
367
-  }
368
-
369
-  ::ng-deep .thinking-details > *:not(summary) {
370
-    background-color: #f0f7ff;
371
-    color: #0d47a1;
372
-    font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
373
-    font-size: 0.95em;
374
-    line-height: 1.5;
375
-  }
376
-
377
-  ::ng-deep .tool-details > *:not(summary) {
378
-    background-color: #f5f0ff;
379
-    color: #4a148c;
380
-    font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
381
-    font-size: 0.95em;
382
-    line-height: 1.5;
383
-  }
384
-
385
-  /* 标签样式(非折叠) */
386
-  ::ng-deep .thinking-tag,
387
-  ::ng-deep .tool-tag,
388
-  ::ng-deep .error-tag {
389
-    display: inline-block;
390
-    padding: 4px 8px;
391
-    border-radius: 4px;
392
-    font-size: 12px;
393
-    font-weight: 500;
394
-    margin-right: 8px;
395
-  }
396
-
397
-  ::ng-deep .thinking-tag {
398
-    background-color: #e3f2fd;
399
-    color: #1565c0;
400
-  }
401
-
402
-  ::ng-deep .tool-tag {
403
-    background-color: #f3e5f5;
404
-    color: #7b1fa2;
405
-  }
406
-
407
-  ::ng-deep .error-tag {
408
-    background-color: #ffebee;
409
-    color: #c62828;
410
-  }
411
-
412
-  /* Markdown内容样式 - 参考原版优化 */
413
-  ::ng-deep h1, 
414
-  ::ng-deep h2, 
415
-  ::ng-deep h3, 
416
-  ::ng-deep h4 {
417
-    margin-top: 1em;
418
-    margin-bottom: 0.5em;
419
-    color: #1976d2;
420
-  }
421
-  
422
-  ::ng-deep p {
423
-    margin: 0.5em 0;
424
-    line-height: 1.5;
425
-  }
426
-  
427
-  ::ng-deep code {
428
-    background: #f5f5f5;
429
-    padding: 2px 4px;
430
-    border-radius: 3px;
431
-    font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
432
-    font-size: 0.9em;
433
-  }
434
-  
435
-  ::ng-deep pre {
436
-    background: #1e1e1e;
437
-    color: #d4d4d4;
438
-    padding: 12px;
439
-    border-radius: 6px;
440
-    overflow: auto;
441
-    margin: 0.75em 0;
442
-    
443
-    code {
444
-      background: transparent;
445
-      padding: 0;
446
-      border-radius: 0;
447
-      font-size: 0.9em;
448
-      line-height: 1.5;
449
-    }
450
-  }
451
-  
452
-  ::ng-deep .prism-code {
453
-    margin: 0;
454
-    padding: 0;
455
-    font-size: 0.9em;
456
-    line-height: 1.5;
457
-  }
458
-  
459
-  ::ng-deep blockquote {
460
-    border-left: 3px solid #1976d2;
461
-    margin: 0.75em 0;
462
-    padding-left: 1em;
463
-    color: #666;
464
-    font-style: italic;
465
-  }
466
-  
467
-  ::ng-deep ul,
468
-  ::ng-deep ol {
469
-    padding-left: 1.5em;
470
-    margin: 0.5em 0;
471
-  }
472
-  
473
-  ::ng-deep table {
474
-    border-collapse: collapse;
475
-    width: 100%;
476
-    margin: 0.75em 0;
477
-    
478
-    th, td {
479
-      border: 1px solid #e0e0e0;
480
-      padding: 6px 10px;
481
-      text-align: left;
482
-    }
483
-    
484
-    th {
485
-      background: #f5f5f5;
486
-      font-weight: 600;
487
-    }
488
-  }
489
-
490
-  /* 自定义details元素图标 */
491
-  ::ng-deep details {
492
-    position: relative;
493
-  }
494
-
495
-  ::ng-deep details summary {
496
-    list-style: none;
497
-  }
498
-
499
-  ::ng-deep details summary::-webkit-details-marker {
500
-    display: none;
501
-  }
502
-
503
-  ::ng-deep details summary::before {
504
-    content: '▶';
505
-    font-size: 10px;
506
-    margin-right: 6px;
507
-    transition: transform 0.2s;
508
-    display: inline-block;
509
-  }
510
-
511
-  ::ng-deep details[open] summary::before {
512
-    transform: rotate(90deg);
513
-  }
514
-
515
-  /* 确保标签内的内容正确显示 */
516
-  ::ng-deep .thinking-details,
517
-  ::ng-deep .tool-details {
518
-    white-space: normal;
519
-    word-break: break-word;
520
-  }
521
-}

+ 2
- 1
src/app/modules/editor/editor.component.ts View File

@@ -18,6 +18,7 @@ import { ChatMessage } from '../../core/models/conversation.model';
18 18
 import { Session } from '../../core/models/session.model';
19 19
 import { GlobalEvent, MessageUpdatedEvent, MessagePartUpdatedEvent } from '../../core/models/event.model';
20 20
 import { MarkdownPipe } from '../../shared/pipes/markdown.pipe';
21
+import { ChatMessageComponent } from '../../shared/components/chat-message/chat-message.component';
21 22
 
22 23
 @Component({
23 24
   selector: 'app-editor',
@@ -32,7 +33,7 @@ import { MarkdownPipe } from '../../shared/pipes/markdown.pipe';
32 33
     MatProgressSpinnerModule,
33 34
     MatToolbarModule,
34 35
     FormsModule,
35
-    MarkdownPipe
36
+    ChatMessageComponent
36 37
   ],
37 38
   templateUrl: './editor.component.html',
38 39
   styleUrl: './editor.component.scss',

+ 25
- 0
src/app/shared/components/chat-message/chat-message.component.html View File

@@ -0,0 +1,25 @@
1
+<div class="chat-message-wrapper" [class.user-message]="message.role === 'user'" [class.ai-message]="message.role === 'assistant'">
2
+  <div class="chat-message-bubble">
3
+    @if (displayHeader) {
4
+      <div class="chat-message-header">
5
+        @if (displayAvatar) {
6
+          <mat-icon class="chat-message-avatar">{{ avatarIcon }}</mat-icon>
7
+        }
8
+        <span class="chat-message-role">{{ avatarLabel }}</span>
9
+        @if (displayTimestamp && formattedTimestamp) {
10
+          <span class="chat-message-time">{{ formattedTimestamp }}</span>
11
+        }
12
+      </div>
13
+    }
14
+    
15
+    <div class="chat-message-content">
16
+      @if (message.loading) {
17
+        <div class="chat-message-loading">
18
+          <mat-spinner diameter="16"></mat-spinner>
19
+          <span>思考中...</span>
20
+        </div>
21
+      }
22
+      <div [innerHTML]="message.content | markdown"></div>
23
+    </div>
24
+  </div>
25
+</div>

+ 348
- 0
src/app/shared/components/chat-message/chat-message.component.scss View File

@@ -0,0 +1,348 @@
1
+/* ChatMessageComponent自包含样式系统 */
2
+/* 消除外部样式干扰,协调所有markdown元素 */
3
+
4
+.chat-message-wrapper {
5
+  display: flex;
6
+  width: 100%;  /* 改为100%,跟随父容器宽度 */
7
+  max-width: none;  /* 移除最大宽度限制 */
8
+  flex-shrink: 0;
9
+  align-self: flex-start;
10
+  margin-bottom: 16px;
11
+  
12
+  &.user-message {
13
+    .chat-message-bubble {
14
+      background-color: #1976d2 !important;
15
+      color: white !important;
16
+      border-radius: 18px 18px 4px 18px !important;
17
+    }
18
+    
19
+    .chat-message-header {
20
+      color: rgba(255, 255, 255, 0.8) !important;
21
+    }
22
+  }
23
+  
24
+  &.ai-message {
25
+    .chat-message-bubble {
26
+      background-color: white !important;
27
+      color: #333 !important;
28
+      border-radius: 18px 18px 18px 4px !important;
29
+      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
30
+    }
31
+    
32
+    .chat-message-header {
33
+      color: #666 !important;
34
+    }
35
+  }
36
+}
37
+
38
+.chat-message-bubble {
39
+  width: 100%;
40
+  box-sizing: border-box;
41
+  padding: 12px 16px;
42
+  word-wrap: break-word;
43
+  word-break: break-word;
44
+}
45
+
46
+.chat-message-header {
47
+  display: flex;
48
+  align-items: center;
49
+  gap: 8px;
50
+  margin-bottom: 8px;
51
+  font-size: 12px;
52
+  
53
+  .chat-message-avatar {
54
+    font-size: 16px;
55
+    height: 16px;
56
+    width: 16px;
57
+  }
58
+  
59
+  .chat-message-role {
60
+    font-weight: 500;
61
+    flex: 1;
62
+  }
63
+  
64
+  .chat-message-time {
65
+    opacity: 0.7;
66
+  }
67
+}
68
+
69
+.chat-message-content {
70
+  font-size: 13px;
71
+  line-height: 1.3;
72
+  white-space: pre-wrap;
73
+  
74
+  .chat-message-loading {
75
+    display: flex;
76
+    align-items: center;
77
+    gap: 8px;
78
+    margin-bottom: 8px;
79
+    color: #666;
80
+    
81
+    span {
82
+      font-size: 12px;
83
+    }
84
+  }
85
+  
86
+  /* ================================= */
87
+  /* 自包含Markdown样式系统            */
88
+  /* 协调所有元素间距,避免外部干扰    */
89
+  /* ================================= */
90
+  ::ng-deep {
91
+    /* 标题 - 统一间距管理 */
92
+    h1, h2, h3, h4, h5, h6 {
93
+      margin: 0.2em 0 0.1em 0;
94
+      padding: 0;
95
+      font-weight: 600;
96
+      color: #1976d2;
97
+      line-height: 1.3;
98
+    }
99
+    
100
+    h1 {
101
+      font-size: 1.4em;
102
+      margin-top: 0.3em;
103
+    }
104
+    
105
+    h2 {
106
+      font-size: 1.3em;
107
+    }
108
+    
109
+    h3 {
110
+      font-size: 1.2em;
111
+    }
112
+    
113
+    h4 {
114
+      font-size: 1.1em;
115
+    }
116
+    
117
+    h5 {
118
+      font-size: 1.05em;
119
+    }
120
+    
121
+    h6 {
122
+      font-size: 1em;
123
+      color: #666;
124
+    }
125
+    
126
+    /* 段落和文本 */
127
+    p {
128
+      margin: 0.1em 0;
129
+      line-height: 1.3;
130
+    }
131
+    
132
+    /* 标题后的段落更紧凑 */
133
+    h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p {
134
+      margin-top: 0.05em;
135
+    }
136
+    
137
+    /* 连续段落 */
138
+    p + p {
139
+      margin-top: 0.05em;
140
+    }
141
+    
142
+    /* 内联代码 - 针对AI消息和用户消息分别处理 */
143
+    :host-context(.ai-message) code:not(pre code),
144
+    .ai-message code:not(pre code) {
145
+      background: #f5f5f5;
146
+      padding: 2px 4px;
147
+      border-radius: 3px;
148
+      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
149
+      font-size: 0.9em;
150
+      color: #d63384;
151
+    }
152
+    
153
+    :host-context(.user-message) code:not(pre code),
154
+    .user-message code:not(pre code) {
155
+      background: rgba(255, 255, 255, 0.2);
156
+      padding: 2px 4px;
157
+      border-radius: 3px;
158
+      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
159
+      font-size: 0.9em;
160
+      color: white;
161
+    }
162
+    
163
+    /* 代码块 - 针对所有消息类型 */
164
+    pre {
165
+      background: #1e1e1e !important;
166
+      color: #d4d4d4 !important;
167
+      padding: 12px !important;
168
+      border-radius: 6px !important;
169
+      overflow-x: auto !important;
170
+      margin: 0.4em 0 !important;
171
+    }
172
+    
173
+    pre code {
174
+      background: transparent !important;
175
+      padding: 0 !important;
176
+      border-radius: 0 !important;
177
+      font-size: 0.9em !important;
178
+      line-height: 1.5 !important;
179
+      color: inherit !important;
180
+      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
181
+    }
182
+    
183
+    /* 引用块 - 针对不同消息类型 */
184
+    :host-context(.ai-message) blockquote,
185
+    .ai-message blockquote {
186
+      border-left: 3px solid #1976d2;
187
+      margin: 0.4em 0;
188
+      padding-left: 12px;
189
+      color: #666;
190
+      font-style: italic;
191
+    }
192
+    
193
+    :host-context(.user-message) blockquote,
194
+    .user-message blockquote {
195
+      border-left: 3px solid rgba(255, 255, 255, 0.5);
196
+      margin: 0.4em 0;
197
+      padding-left: 12px;
198
+      color: rgba(255, 255, 255, 0.9);
199
+      font-style: italic;
200
+    }
201
+    
202
+    /* 列表 */
203
+    ul, ol {
204
+      padding-left: 1.2em;
205
+      margin: 0.2em 0;
206
+    }
207
+    
208
+    li {
209
+      margin: 0.05em 0;
210
+      line-height: 1.3;
211
+    }
212
+    
213
+    /* 表格 */
214
+    table {
215
+      border-collapse: collapse;
216
+      width: 100%;
217
+      margin: 0.4em 0;
218
+      font-size: 0.9em;
219
+      
220
+      th, td {
221
+        border: 1px solid #e0e0e0;
222
+        padding: 4px 8px;
223
+        text-align: left;
224
+      }
225
+      
226
+      th {
227
+        background: #f5f5f5;
228
+        font-weight: 600;
229
+      }
230
+    }
231
+    
232
+    /* 图片 */
233
+    img {
234
+      max-width: 100%;
235
+      height: auto;
236
+      border-radius: 4px;
237
+      margin: 0.2em 0;
238
+    }
239
+    
240
+    /* 链接 - 针对不同消息类型 */
241
+    :host-context(.ai-message) a,
242
+    .ai-message a {
243
+      color: #1976d2;
244
+      text-decoration: none;
245
+      
246
+      &:hover {
247
+        text-decoration: underline;
248
+      }
249
+    }
250
+    
251
+    :host-context(.user-message) a,
252
+    .user-message a {
253
+      color: white;
254
+      text-decoration: underline;
255
+      text-decoration-color: rgba(255, 255, 255, 0.5);
256
+      
257
+      &:hover {
258
+        text-decoration-color: white;
259
+      }
260
+    }
261
+    
262
+    /* 水平线 */
263
+    hr {
264
+      border: none;
265
+      border-top: 1px solid #e0e0e0;
266
+      margin: 0.4em 0;
267
+    }
268
+    
269
+    /* 思考部分样式 */
270
+    .thinking-container {
271
+      background-color: #f0f7ff;
272
+      border: 1px solid #bbdefb;
273
+      border-radius: 6px;
274
+      margin: 0.4em 0;
275
+      overflow: hidden;
276
+    }
277
+    
278
+    .thinking-container > strong {
279
+      display: block;
280
+      padding: 4px 8px;
281
+      background-color: #e3f2fd;
282
+      color: #0d47a1;
283
+      font-weight: 600;
284
+      font-size: 12px;
285
+      border-bottom: 1px solid #bbdefb;
286
+    }
287
+    
288
+    .thinking-body {
289
+      padding: 8px;
290
+      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
291
+      font-size: 0.9em;
292
+      line-height: 1.4;
293
+      color: #0d47a1;
294
+      white-space: pre-wrap;
295
+      word-break: break-word;
296
+    }
297
+    
298
+    /* 工具部分样式 */
299
+    .tool-container {
300
+      background-color: #f5f0ff;
301
+      border: 1px solid #e1bee7;
302
+      border-radius: 6px;
303
+      margin: 0.4em 0;
304
+      overflow: hidden;
305
+    }
306
+    
307
+    .tool-container > strong {
308
+      display: block;
309
+      padding: 4px 8px;
310
+      background-color: #f3e5f5;
311
+      color: #7b1fa2;
312
+      font-weight: 600;
313
+      font-size: 12px;
314
+      border-bottom: 1px solid #e1bee7;
315
+    }
316
+    
317
+    .tool-body {
318
+      padding: 8px;
319
+      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
320
+      font-size: 0.9em;
321
+      line-height: 1.4;
322
+      color: #4a148c;
323
+      white-space: pre-wrap;
324
+      word-break: break-word;
325
+    }
326
+    
327
+    /* 错误标签样式 */
328
+    .error-tag {
329
+      display: inline-block;
330
+      padding: 2px 5px;
331
+      border-radius: 3px;
332
+      font-size: 11px;
333
+      font-weight: 500;
334
+      margin-right: 4px;
335
+      background-color: #ffebee;
336
+      color: #c62828;
337
+    }
338
+    
339
+    /* 文本样式 */
340
+    strong {
341
+      font-weight: 600;
342
+    }
343
+    
344
+    em {
345
+      font-style: italic;
346
+    }
347
+  }
348
+}

+ 75
- 0
src/app/shared/components/chat-message/chat-message.component.ts View File

@@ -0,0 +1,75 @@
1
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
2
+import { CommonModule } from '@angular/common';
3
+import { MatIconModule } from '@angular/material/icon';
4
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
5
+import { MarkdownPipe } from '../../pipes/markdown.pipe';
6
+
7
+export interface ChatMessageInput {
8
+  role: 'user' | 'assistant' | 'system';
9
+  content: string;               // markdown内容
10
+  timestamp?: Date;              // 可选时间戳
11
+  loading?: boolean;            // 加载状态
12
+  showAvatar?: boolean;         // 是否显示头像(默认true)
13
+  showTimestamp?: boolean;      // 是否显示时间(默认true)
14
+  showHeader?: boolean;         // 是否显示头部(默认true)
15
+}
16
+
17
+@Component({
18
+  selector: 'app-chat-message',
19
+  standalone: true,
20
+  imports: [
21
+    CommonModule,
22
+    MatIconModule,
23
+    MatProgressSpinnerModule,
24
+    MarkdownPipe
25
+  ],
26
+  templateUrl: './chat-message.component.html',
27
+  styleUrl: './chat-message.component.scss',
28
+  changeDetection: ChangeDetectionStrategy.OnPush
29
+})
30
+export class ChatMessageComponent {
31
+  @Input() message!: ChatMessageInput;
32
+
33
+  // 默认值设置
34
+  get displayAvatar(): boolean {
35
+    return this.message.showAvatar !== false;
36
+  }
37
+
38
+  get displayTimestamp(): boolean {
39
+    return this.message.showTimestamp !== false;
40
+  }
41
+
42
+  get displayHeader(): boolean {
43
+    return this.message.showHeader !== false;
44
+  }
45
+
46
+  get formattedTimestamp(): string {
47
+    if (!this.message.timestamp) return '';
48
+    return this.formatTime(this.message.timestamp);
49
+  }
50
+
51
+  get avatarIcon(): string {
52
+    switch (this.message.role) {
53
+      case 'user': return 'person';
54
+      case 'assistant': return 'smart_toy';
55
+      case 'system': return 'computer';
56
+      default: return 'person';
57
+    }
58
+  }
59
+
60
+  get avatarLabel(): string {
61
+    switch (this.message.role) {
62
+      case 'user': return '用户';
63
+      case 'assistant': return 'AI助手';
64
+      case 'system': return '系统';
65
+      default: return '用户';
66
+    }
67
+  }
68
+
69
+  private formatTime(date: Date): string {
70
+    const d = new Date(date);
71
+    const hours = d.getHours().toString().padStart(2, '0');
72
+    const minutes = d.getMinutes().toString().padStart(2, '0');
73
+    return `${hours}:${minutes}`;
74
+  }
75
+}

+ 13
- 6
src/app/shared/pipes/markdown.pipe.ts View File

@@ -52,13 +52,13 @@ export class MarkdownPipe implements PipeTransform {
52 52
   }
53 53
 
54 54
   /**
55
-   * 预处理:将文本标签替换为美化版本和可折叠区域
55
+   * 预处理:将文本标签替换为美化版本
56 56
    */
57 57
   private preprocessTags(content: string): string {
58
-    // 处理 [思考] 标签及其内容
58
+    // 处理 [思考] 标签及其内容(不折叠,普通显示)
59 59
     content = this.processTagWithContent(content, '思考', '💭', 'thinking');
60 60
     
61
-    // 处理 [工具] 标签及其内容
61
+    // 处理 [工具] 标签及其内容(不折叠)
62 62
     content = this.processTagWithContent(content, '工具', '🛠️', 'tool');
63 63
     
64 64
     // 处理 [错误] 标签
@@ -68,7 +68,7 @@ export class MarkdownPipe implements PipeTransform {
68 68
   }
69 69
 
70 70
   /**
71
-   * 处理标签及其后的内容,转换为可折叠区域
71
+   * 处理标签及其后的内容
72 72
    */
73 73
   private processTagWithContent(content: string, tagName: string, emoji: string, cssClass: string): string {
74 74
     // 正则表达式匹配 [标签] 及其后的内容(直到下一个标签或字符串结束)
@@ -78,10 +78,17 @@ export class MarkdownPipe implements PipeTransform {
78 78
       // 清理内容前后的空白
79 79
       const trimmedContent = tagContent.trim();
80 80
       if (!trimmedContent) {
81
-        return `<details class="${cssClass}-details"><summary class="${cssClass}-summary">${emoji} ${tagName}</summary></details>`;
81
+        return `<div class="${cssClass}-container">${emoji} ${tagName}</div>`;
82 82
       }
83 83
       
84
-      return `<details class="${cssClass}-details" ${cssClass === 'thinking' || cssClass === 'tool' ? 'open' : ''}><summary class="${cssClass}-summary">${emoji} ${tagName}</summary>${trimmedContent}</details>`;
84
+      // 思考部分和工具部分都不折叠
85
+      if (cssClass === 'thinking') {
86
+        return `<div class="thinking-container"><strong>${emoji} ${tagName}</strong><div class="thinking-body">${trimmedContent}</div></div>`;
87
+      } else if (cssClass === 'tool') {
88
+        return `<div class="tool-container"><strong>${emoji} ${tagName}</strong><div class="tool-body">${trimmedContent}</div></div>`;
89
+      }
90
+      
91
+      return `<div class="${cssClass}-container">${emoji} ${tagName}<br/>${trimmedContent}</div>`;
85 92
     });
86 93
   }
87 94
 

Loading…
Cancel
Save