Bladeren bron

Release v0.2.21303

qdy 2 weken geleden
commit
1b42c7b062

+ 1
- 1
src/app/components/conversation.component.html Bestand weergeven

@@ -9,7 +9,7 @@
9 9
               <span class="message-time">{{ message.timestamp | date:'HH:mm' }}</span>
10 10
             </div>
11 11
             <div class="message-content">
12
-              <div [innerHTML]="formatContent(message.content)"></div>
12
+              <div [innerHTML]="message.content | markdown"></div>
13 13
               <mat-spinner *ngIf="message.loading" diameter="20" class="loading-spinner"></mat-spinner>
14 14
             </div>
15 15
           </div>

+ 135
- 0
src/app/components/conversation.component.scss Bestand weergeven

@@ -99,3 +99,138 @@
99 99
       color: #666;
100 100
       background: #f5f5f5;
101 101
     }
102
+
103
+    /* 可折叠标签样式 */
104
+    details {
105
+      margin: 8px 0;
106
+      border-radius: 8px;
107
+      overflow: hidden;
108
+    }
109
+
110
+    .thinking-details {
111
+      border: 1px solid #e3f2fd;
112
+      background-color: #f5fbff;
113
+    }
114
+
115
+    .tool-details {
116
+      border: 1px solid #f3e5f5;
117
+      background-color: #faf5ff;
118
+    }
119
+
120
+    .thinking-summary,
121
+    .tool-summary {
122
+      padding: 8px 12px;
123
+      cursor: pointer;
124
+      font-weight: 500;
125
+      display: flex;
126
+      align-items: center;
127
+      gap: 6px;
128
+      user-select: none;
129
+    }
130
+
131
+    .thinking-summary {
132
+      color: #1565c0;
133
+      background-color: #e3f2fd;
134
+    }
135
+
136
+    .tool-summary {
137
+      color: #7b1fa2;
138
+      background-color: #f3e5f5;
139
+    }
140
+
141
+    /* details元素内的内容样式 */
142
+    .thinking-details > *:not(summary),
143
+    .tool-details > *:not(summary) {
144
+      padding: 12px;
145
+      border-top: 1px solid rgba(0, 0, 0, 0.05);
146
+    }
147
+
148
+    .thinking-details > *:not(summary) {
149
+      background-color: #f5fbff;
150
+    }
151
+
152
+    .tool-details > *:not(summary) {
153
+      background-color: #faf5ff;
154
+    }
155
+
156
+    /* 标签样式(非折叠) */
157
+    .thinking-tag,
158
+    .tool-tag,
159
+    .error-tag {
160
+      display: inline-block;
161
+      padding: 4px 8px;
162
+      border-radius: 4px;
163
+      font-size: 12px;
164
+      font-weight: 500;
165
+      margin-right: 8px;
166
+    }
167
+
168
+    .thinking-tag {
169
+      background-color: #e3f2fd;
170
+      color: #1565c0;
171
+    }
172
+
173
+    .tool-tag {
174
+      background-color: #f3e5f5;
175
+      color: #7b1fa2;
176
+    }
177
+
178
+    .error-tag {
179
+      background-color: #ffebee;
180
+      color: #c62828;
181
+    }
182
+
183
+    /* 确保markdown内容样式正确 */
184
+    .message-content :deep(*) {
185
+      margin-top: 8px;
186
+      margin-bottom: 8px;
187
+    }
188
+
189
+    .message-content :deep(ul),
190
+    .message-content :deep(ol) {
191
+      padding-left: 24px;
192
+    }
193
+
194
+    .message-content :deep(code) {
195
+      background-color: #f5f5f5;
196
+      padding: 2px 4px;
197
+      border-radius: 3px;
198
+      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
199
+      font-size: 0.9em;
200
+    }
201
+
202
+    .message-content :deep(pre) {
203
+      margin: 12px 0;
204
+    }
205
+
206
+    /* 自定义details元素图标 */
207
+    .message-content :deep(details) {
208
+      position: relative;
209
+    }
210
+
211
+    .message-content :deep(details summary) {
212
+      list-style: none;
213
+    }
214
+
215
+    .message-content :deep(details summary::-webkit-details-marker) {
216
+      display: none;
217
+    }
218
+
219
+    .message-content :deep(details summary::before) {
220
+      content: '▶';
221
+      font-size: 10px;
222
+      margin-right: 6px;
223
+      transition: transform 0.2s;
224
+      display: inline-block;
225
+    }
226
+
227
+    .message-content :deep(details[open] summary::before) {
228
+      transform: rotate(90deg);
229
+    }
230
+
231
+    /* 确保标签内的内容正确显示 */
232
+    .message-content :deep(.thinking-details),
233
+    .message-content :deep(.tool-details) {
234
+      white-space: normal;
235
+      word-break: break-word;
236
+    }

+ 4
- 5
src/app/components/conversation.component.ts Bestand weergeven

@@ -15,6 +15,7 @@ import { EventService } from '../services/event.service';
15 15
 import { ChatMessage } from '../models/conversation.model';
16 16
 import { Session } from '../models/session.model';
17 17
 import { GlobalEvent, MessageUpdatedEvent, MessagePartUpdatedEvent, SessionUpdatedEvent } from '../models/event.model';
18
+import { MarkdownPipe } from '../shared/pipes/markdown.pipe';
18 19
 
19 20
 @Component({
20 21
   selector: 'app-conversation',
@@ -27,7 +28,8 @@ import { GlobalEvent, MessageUpdatedEvent, MessagePartUpdatedEvent, SessionUpdat
27 28
     MatInputModule,
28 29
     MatFormFieldModule,
29 30
     MatProgressSpinnerModule,
30
-    FormsModule
31
+    FormsModule,
32
+    MarkdownPipe
31 33
   ],
32 34
   templateUrl: './conversation.component.html',
33 35
   styleUrl: './conversation.component.scss',
@@ -530,10 +532,7 @@ export class ConversationComponent implements OnInit, OnDestroy, AfterViewChecke
530 532
     this.focusInput();
531 533
   }
532 534
 
533
-  formatContent(content: string): string {
534
-    // 简单的格式化,可扩展为Markdown渲染
535
-    return content.replace(/\n/g, '<br>');
536
-  }
535
+
537 536
 
538 537
   private scrollToBottom() {
539 538
     try {

+ 110
- 0
src/app/shared/pipes/markdown.pipe.ts Bestand weergeven

@@ -0,0 +1,110 @@
1
+import { Pipe, PipeTransform } from '@angular/core';
2
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
3
+import { marked } from 'marked';
4
+import Prism from 'prismjs';
5
+
6
+// 导入需要的语言支持
7
+import 'prismjs/components/prism-clike';
8
+import 'prismjs/components/prism-markup';
9
+import 'prismjs/components/prism-javascript';
10
+import 'prismjs/components/prism-typescript';
11
+import 'prismjs/components/prism-python';
12
+import 'prismjs/components/prism-go';
13
+import 'prismjs/components/prism-java';
14
+import 'prismjs/components/prism-csharp';
15
+import 'prismjs/components/prism-php';
16
+import 'prismjs/components/prism-ruby';
17
+import 'prismjs/components/prism-bash';
18
+import 'prismjs/components/prism-sql';
19
+import 'prismjs/components/prism-json';
20
+import 'prismjs/components/prism-yaml';
21
+import 'prismjs/components/prism-markdown';
22
+
23
+@Pipe({
24
+  name: 'markdown',
25
+  standalone: true
26
+})
27
+export class MarkdownPipe implements PipeTransform {
28
+  constructor(private sanitizer: DomSanitizer) {}
29
+
30
+  transform(value: string): SafeHtml {
31
+    if (!value) {
32
+      return '';
33
+    }
34
+
35
+    // 预处理:将标签替换为美化版本
36
+    const processedContent = this.preprocessTags(value);
37
+    
38
+    // 配置marked(使用基本配置)
39
+    marked.setOptions({
40
+      gfm: true,
41
+      breaks: true
42
+    });
43
+
44
+    // 解析markdown
45
+    let html = marked.parse(processedContent) as string;
46
+    
47
+    // 后处理:添加代码高亮和样式
48
+    html = this.postprocessCodeBlocks(html);
49
+    
50
+    // 安全注入HTML
51
+    return this.sanitizer.bypassSecurityTrustHtml(html);
52
+  }
53
+
54
+  /**
55
+   * 预处理:将文本标签替换为美化版本和可折叠区域
56
+   */
57
+  private preprocessTags(content: string): string {
58
+    // 处理 [思考] 标签及其内容
59
+    content = this.processTagWithContent(content, '思考', '💭', 'thinking');
60
+    
61
+    // 处理 [工具] 标签及其内容
62
+    content = this.processTagWithContent(content, '工具', '🛠️', 'tool');
63
+    
64
+    // 处理 [错误] 标签
65
+    content = content.replace(/\[错误\]/g, '<span class="error-tag">❌ 错误</span>');
66
+    
67
+    return content;
68
+  }
69
+
70
+  /**
71
+   * 处理标签及其后的内容,转换为可折叠区域
72
+   */
73
+  private processTagWithContent(content: string, tagName: string, emoji: string, cssClass: string): string {
74
+    // 正则表达式匹配 [标签] 及其后的内容(直到下一个标签或字符串结束)
75
+    const pattern = new RegExp(`\\[${tagName}\\]\\s*([\\s\\S]*?)(?=\\n\\s*\\[|$)`, 'g');
76
+    
77
+    return content.replace(pattern, (match, tagContent) => {
78
+      // 清理内容前后的空白
79
+      const trimmedContent = tagContent.trim();
80
+      if (!trimmedContent) {
81
+        return `<details class="${cssClass}-details"><summary class="${cssClass}-summary">${emoji} ${tagName}</summary></details>`;
82
+      }
83
+      
84
+      return `<details class="${cssClass}-details" ${cssClass === 'thinking' || cssClass === 'tool' ? '' : 'open'}><summary class="${cssClass}-summary">${emoji} ${tagName}</summary>${trimmedContent}</details>`;
85
+    });
86
+  }
87
+
88
+  /**
89
+   * 后处理:添加代码高亮和样式
90
+   */
91
+  private postprocessCodeBlocks(html: string): string {
92
+    // 使用正则表达式匹配代码块
93
+    const codeBlockRegex = /<pre><code\s*(?:class="language-([^"]+)")?>([\s\S]*?)<\/code><\/pre>/gi;
94
+    
95
+    html = html.replace(codeBlockRegex, (match, lang, code) => {
96
+      const language = lang || 'plaintext';
97
+      
98
+      try {
99
+        // 使用Prism高亮代码
100
+        const highlighted = Prism.highlight(code, Prism.languages[language] || Prism.languages['plaintext'], language);
101
+        return `<pre class="prism-code language-${language}"><code class="language-${language}">${highlighted}</code></pre>`;
102
+      } catch (e) {
103
+        // 如果高亮失败,返回原始代码块
104
+        return `<pre class="prism-code language-${language}"><code class="language-${language}">${code}</code></pre>`;
105
+      }
106
+    });
107
+    
108
+    return html;
109
+  }
110
+}

Laden…
Annuleren
Opslaan