tabby-ai-assistant 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chat/ai-sidebar.component.d.ts +147 -0
- package/dist/components/chat/chat-interface.component.d.ts +38 -6
- package/dist/components/settings/general-settings.component.d.ts +6 -3
- package/dist/components/settings/provider-config.component.d.ts +25 -12
- package/dist/components/terminal/command-preview.component.d.ts +38 -0
- package/dist/index-full.d.ts +8 -0
- package/dist/index-minimal.d.ts +3 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1 -2
- package/dist/providers/tabby/ai-config.provider.d.ts +57 -5
- package/dist/providers/tabby/ai-hotkey.provider.d.ts +8 -14
- package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +8 -9
- package/dist/services/chat/ai-sidebar.service.d.ts +89 -0
- package/dist/services/chat/chat-history.service.d.ts +78 -0
- package/dist/services/chat/chat-session.service.d.ts +57 -2
- package/dist/services/context/compaction.d.ts +90 -0
- package/dist/services/context/manager.d.ts +69 -0
- package/dist/services/context/memory.d.ts +116 -0
- package/dist/services/context/token-budget.d.ts +105 -0
- package/dist/services/core/ai-assistant.service.d.ts +40 -1
- package/dist/services/core/checkpoint.service.d.ts +130 -0
- package/dist/services/platform/escape-sequence.service.d.ts +132 -0
- package/dist/services/platform/platform-detection.service.d.ts +146 -0
- package/dist/services/providers/anthropic-provider.service.d.ts +5 -0
- package/dist/services/providers/base-provider.service.d.ts +6 -1
- package/dist/services/providers/glm-provider.service.d.ts +5 -0
- package/dist/services/providers/minimax-provider.service.d.ts +10 -1
- package/dist/services/providers/ollama-provider.service.d.ts +76 -0
- package/dist/services/providers/openai-compatible.service.d.ts +5 -0
- package/dist/services/providers/openai-provider.service.d.ts +5 -0
- package/dist/services/providers/vllm-provider.service.d.ts +82 -0
- package/dist/services/terminal/buffer-analyzer.service.d.ts +128 -0
- package/dist/services/terminal/terminal-manager.service.d.ts +185 -0
- package/dist/services/terminal/terminal-tools.service.d.ts +79 -0
- package/dist/types/ai.types.d.ts +92 -0
- package/dist/types/provider.types.d.ts +1 -1
- package/package.json +7 -10
- package/src/components/chat/ai-sidebar.component.ts +945 -0
- package/src/components/chat/chat-input.component.html +9 -24
- package/src/components/chat/chat-input.component.scss +3 -2
- package/src/components/chat/chat-interface.component.html +77 -69
- package/src/components/chat/chat-interface.component.scss +54 -4
- package/src/components/chat/chat-interface.component.ts +250 -34
- package/src/components/chat/chat-settings.component.scss +4 -4
- package/src/components/chat/chat-settings.component.ts +22 -11
- package/src/components/common/error-message.component.html +15 -0
- package/src/components/common/error-message.component.scss +77 -0
- package/src/components/common/error-message.component.ts +2 -96
- package/src/components/common/loading-spinner.component.html +4 -0
- package/src/components/common/loading-spinner.component.scss +57 -0
- package/src/components/common/loading-spinner.component.ts +2 -63
- package/src/components/security/consent-dialog.component.html +22 -0
- package/src/components/security/consent-dialog.component.scss +34 -0
- package/src/components/security/consent-dialog.component.ts +2 -55
- package/src/components/security/password-prompt.component.html +19 -0
- package/src/components/security/password-prompt.component.scss +30 -0
- package/src/components/security/password-prompt.component.ts +2 -54
- package/src/components/security/risk-confirm-dialog.component.html +8 -12
- package/src/components/security/risk-confirm-dialog.component.scss +8 -5
- package/src/components/security/risk-confirm-dialog.component.ts +6 -6
- package/src/components/settings/ai-settings-tab.component.html +16 -20
- package/src/components/settings/ai-settings-tab.component.scss +8 -5
- package/src/components/settings/ai-settings-tab.component.ts +12 -12
- package/src/components/settings/general-settings.component.html +8 -17
- package/src/components/settings/general-settings.component.scss +6 -3
- package/src/components/settings/general-settings.component.ts +62 -22
- package/src/components/settings/provider-config.component.html +19 -39
- package/src/components/settings/provider-config.component.scss +182 -39
- package/src/components/settings/provider-config.component.ts +119 -7
- package/src/components/settings/security-settings.component.scss +1 -1
- package/src/components/terminal/ai-toolbar-button.component.html +8 -0
- package/src/components/terminal/ai-toolbar-button.component.scss +20 -0
- package/src/components/terminal/ai-toolbar-button.component.ts +2 -30
- package/src/components/terminal/command-preview.component.html +61 -0
- package/src/components/terminal/command-preview.component.scss +72 -0
- package/src/components/terminal/command-preview.component.ts +127 -140
- package/src/components/terminal/command-suggestion.component.html +23 -0
- package/src/components/terminal/command-suggestion.component.scss +55 -0
- package/src/components/terminal/command-suggestion.component.ts +2 -77
- package/src/index-minimal.ts +32 -0
- package/src/index.ts +94 -11
- package/src/index.ts.backup +165 -0
- package/src/providers/tabby/ai-config.provider.ts +60 -51
- package/src/providers/tabby/ai-hotkey.provider.ts +23 -39
- package/src/providers/tabby/ai-settings-tab.provider.ts +2 -2
- package/src/providers/tabby/ai-toolbar-button.provider.ts +29 -24
- package/src/services/chat/ai-sidebar.service.ts +258 -0
- package/src/services/chat/chat-history.service.ts +308 -0
- package/src/services/chat/chat-history.service.ts.backup +239 -0
- package/src/services/chat/chat-session.service.ts +276 -3
- package/src/services/context/compaction.ts +483 -0
- package/src/services/context/manager.ts +442 -0
- package/src/services/context/memory.ts +519 -0
- package/src/services/context/token-budget.ts +422 -0
- package/src/services/core/ai-assistant.service.ts +280 -5
- package/src/services/core/ai-provider-manager.service.ts +2 -2
- package/src/services/core/checkpoint.service.ts +619 -0
- package/src/services/platform/escape-sequence.service.ts +499 -0
- package/src/services/platform/platform-detection.service.ts +494 -0
- package/src/services/providers/anthropic-provider.service.ts +28 -1
- package/src/services/providers/base-provider.service.ts +7 -1
- package/src/services/providers/glm-provider.service.ts +28 -1
- package/src/services/providers/minimax-provider.service.ts +209 -11
- package/src/services/providers/ollama-provider.service.ts +445 -0
- package/src/services/providers/openai-compatible.service.ts +9 -0
- package/src/services/providers/openai-provider.service.ts +9 -0
- package/src/services/providers/vllm-provider.service.ts +463 -0
- package/src/services/security/risk-assessment.service.ts +6 -2
- package/src/services/terminal/buffer-analyzer.service.ts +594 -0
- package/src/services/terminal/terminal-manager.service.ts +748 -0
- package/src/services/terminal/terminal-tools.service.ts +441 -0
- package/src/styles/ai-assistant.scss +78 -6
- package/src/types/ai.types.ts +144 -0
- package/src/types/provider.types.ts +1 -1
- package/tsconfig.json +9 -9
- package/webpack.config.js +28 -6
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
1
|
+
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
|
|
2
2
|
import { Subject } from 'rxjs';
|
|
3
3
|
import { takeUntil } from 'rxjs/operators';
|
|
4
|
-
import { ChatMessage, MessageRole } from '../../types/ai.types';
|
|
4
|
+
import { ChatMessage, MessageRole, StreamEvent } from '../../types/ai.types';
|
|
5
5
|
import { AiAssistantService } from '../../services/core/ai-assistant.service';
|
|
6
6
|
import { ConfigProviderService } from '../../services/core/config-provider.service';
|
|
7
7
|
import { LoggerService } from '../../services/core/logger.service';
|
|
8
|
+
import { ChatHistoryService } from '../../services/chat/chat-history.service';
|
|
8
9
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|
9
10
|
|
|
10
11
|
@Component({
|
|
@@ -12,50 +13,105 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|
|
12
13
|
templateUrl: './chat-interface.component.html',
|
|
13
14
|
styleUrls: ['./chat-interface.component.scss']
|
|
14
15
|
})
|
|
15
|
-
export class ChatInterfaceComponent implements OnInit, OnDestroy {
|
|
16
|
+
export class ChatInterfaceComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|
17
|
+
@ViewChild('chatContainer') chatContainerRef!: ElementRef;
|
|
18
|
+
|
|
16
19
|
messages: ChatMessage[] = [];
|
|
17
20
|
isLoading = false;
|
|
18
21
|
currentProvider: string = '';
|
|
22
|
+
currentSessionId: string = '';
|
|
23
|
+
showScrollTop = false;
|
|
24
|
+
showScrollBottom = false;
|
|
19
25
|
private destroy$ = new Subject<void>();
|
|
26
|
+
private shouldScrollToBottom = false;
|
|
20
27
|
|
|
21
28
|
constructor(
|
|
22
29
|
private aiService: AiAssistantService,
|
|
23
30
|
private config: ConfigProviderService,
|
|
24
31
|
private logger: LoggerService,
|
|
25
|
-
private modal: NgbModal
|
|
26
|
-
|
|
32
|
+
private modal: NgbModal,
|
|
33
|
+
private chatHistory: ChatHistoryService
|
|
34
|
+
) { }
|
|
27
35
|
|
|
28
36
|
ngOnInit(): void {
|
|
37
|
+
// 生成或加载会话 ID
|
|
38
|
+
this.currentSessionId = this.generateSessionId();
|
|
39
|
+
|
|
29
40
|
// 加载当前提供商信息
|
|
30
41
|
this.loadCurrentProvider();
|
|
31
42
|
|
|
32
43
|
// 加载聊天历史
|
|
33
44
|
this.loadChatHistory();
|
|
34
45
|
|
|
35
|
-
//
|
|
36
|
-
this.
|
|
46
|
+
// 发送欢迎消息(仅在没有历史记录时)
|
|
47
|
+
if (this.messages.length === 0) {
|
|
48
|
+
this.sendWelcomeMessage();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 延迟检查滚动状态(等待 DOM 渲染)
|
|
52
|
+
setTimeout(() => this.checkScrollState(), 100);
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
ngOnDestroy(): void {
|
|
56
|
+
// 保存当前会话
|
|
57
|
+
this.saveChatHistory();
|
|
40
58
|
this.destroy$.next();
|
|
41
59
|
this.destroy$.complete();
|
|
42
60
|
}
|
|
43
61
|
|
|
62
|
+
ngAfterViewChecked(): void {
|
|
63
|
+
if (this.shouldScrollToBottom) {
|
|
64
|
+
this.performScrollToBottom();
|
|
65
|
+
this.shouldScrollToBottom = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
/**
|
|
45
70
|
* 加载当前提供商信息
|
|
46
71
|
*/
|
|
47
72
|
private loadCurrentProvider(): void {
|
|
48
|
-
const
|
|
49
|
-
|
|
73
|
+
const defaultProvider = this.config.getDefaultProvider();
|
|
74
|
+
if (defaultProvider) {
|
|
75
|
+
const providerConfig = this.config.getProviderConfig(defaultProvider);
|
|
76
|
+
this.currentProvider = providerConfig?.displayName || defaultProvider;
|
|
77
|
+
} else {
|
|
78
|
+
// 尝试获取第一个已配置的提供商
|
|
79
|
+
const allConfigs = this.config.getAllProviderConfigs();
|
|
80
|
+
const configuredProviders = Object.keys(allConfigs).filter(k => allConfigs[k]?.apiKey);
|
|
81
|
+
if (configuredProviders.length > 0) {
|
|
82
|
+
const firstProvider = configuredProviders[0];
|
|
83
|
+
const providerConfig = allConfigs[firstProvider];
|
|
84
|
+
this.currentProvider = providerConfig?.displayName || firstProvider;
|
|
85
|
+
this.config.setDefaultProvider(firstProvider);
|
|
86
|
+
} else {
|
|
87
|
+
this.currentProvider = '未配置';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
50
90
|
}
|
|
51
91
|
|
|
52
92
|
/**
|
|
53
93
|
* 加载聊天历史
|
|
54
94
|
*/
|
|
55
95
|
private loadChatHistory(): void {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
try {
|
|
97
|
+
// 尝试加载最近的会话
|
|
98
|
+
const recentSessions = this.chatHistory.getRecentSessions(1);
|
|
99
|
+
if (recentSessions.length > 0) {
|
|
100
|
+
const lastSession = recentSessions[0];
|
|
101
|
+
this.currentSessionId = lastSession.sessionId;
|
|
102
|
+
this.messages = lastSession.messages.map(msg => ({
|
|
103
|
+
...msg,
|
|
104
|
+
timestamp: new Date(msg.timestamp)
|
|
105
|
+
}));
|
|
106
|
+
this.logger.info('Loaded chat history', {
|
|
107
|
+
sessionId: this.currentSessionId,
|
|
108
|
+
messageCount: this.messages.length
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
this.logger.error('Failed to load chat history', error);
|
|
113
|
+
this.messages = [];
|
|
114
|
+
}
|
|
59
115
|
}
|
|
60
116
|
|
|
61
117
|
/**
|
|
@@ -97,20 +153,59 @@ export class ChatInterfaceComponent implements OnInit, OnDestroy {
|
|
|
97
153
|
// 显示加载状态
|
|
98
154
|
this.isLoading = true;
|
|
99
155
|
|
|
156
|
+
// 创建一个临时的 AI 消息用于流式更新
|
|
157
|
+
const aiMessage: ChatMessage = {
|
|
158
|
+
id: this.generateId(),
|
|
159
|
+
role: MessageRole.ASSISTANT,
|
|
160
|
+
content: '', // 初始为空
|
|
161
|
+
timestamp: new Date()
|
|
162
|
+
};
|
|
163
|
+
this.messages.push(aiMessage);
|
|
164
|
+
|
|
100
165
|
try {
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
messages: this.messages,
|
|
166
|
+
// 使用流式 API
|
|
167
|
+
this.aiService.chatStream({
|
|
168
|
+
messages: this.messages.slice(0, -1), // 排除刚添加的空 AI 消息
|
|
104
169
|
maxTokens: 1000,
|
|
105
170
|
temperature: 0.7
|
|
171
|
+
}).pipe(
|
|
172
|
+
takeUntil(this.destroy$)
|
|
173
|
+
).subscribe({
|
|
174
|
+
next: (event) => {
|
|
175
|
+
// 文本增量 - 逐字显示
|
|
176
|
+
if (event.type === 'text_delta' && event.textDelta) {
|
|
177
|
+
aiMessage.content += event.textDelta;
|
|
178
|
+
this.shouldScrollToBottom = true;
|
|
179
|
+
}
|
|
180
|
+
// 工具调用开始 - 显示提示
|
|
181
|
+
else if (event.type === 'tool_use_start') {
|
|
182
|
+
aiMessage.content += '\n\n🔧 正在执行工具...';
|
|
183
|
+
this.shouldScrollToBottom = true;
|
|
184
|
+
}
|
|
185
|
+
// 工具调用完成 - 更新状态
|
|
186
|
+
else if (event.type === 'tool_use_end') {
|
|
187
|
+
aiMessage.content = aiMessage.content.replace('🔧 正在执行工具...', '✅ 工具执行完成');
|
|
188
|
+
this.shouldScrollToBottom = true;
|
|
189
|
+
}
|
|
190
|
+
// 消息结束
|
|
191
|
+
else if (event.type === 'message_end') {
|
|
192
|
+
this.logger.info('Stream completed');
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
error: (error) => {
|
|
196
|
+
this.logger.error('Stream error', error);
|
|
197
|
+
aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
198
|
+
this.isLoading = false;
|
|
199
|
+
this.shouldScrollToBottom = true;
|
|
200
|
+
this.saveChatHistory();
|
|
201
|
+
},
|
|
202
|
+
complete: () => {
|
|
203
|
+
this.isLoading = false;
|
|
204
|
+
this.saveChatHistory();
|
|
205
|
+
this.shouldScrollToBottom = true;
|
|
206
|
+
}
|
|
106
207
|
});
|
|
107
208
|
|
|
108
|
-
// 添加AI响应
|
|
109
|
-
this.messages.push(response.message);
|
|
110
|
-
|
|
111
|
-
// 保存聊天历史
|
|
112
|
-
this.saveChatHistory();
|
|
113
|
-
|
|
114
209
|
} catch (error) {
|
|
115
210
|
this.logger.error('Failed to send message', error);
|
|
116
211
|
|
|
@@ -122,9 +217,7 @@ export class ChatInterfaceComponent implements OnInit, OnDestroy {
|
|
|
122
217
|
timestamp: new Date()
|
|
123
218
|
};
|
|
124
219
|
this.messages.push(errorMessage);
|
|
125
|
-
} finally {
|
|
126
220
|
this.isLoading = false;
|
|
127
|
-
// 滚动到底部
|
|
128
221
|
setTimeout(() => this.scrollToBottom(), 0);
|
|
129
222
|
}
|
|
130
223
|
}
|
|
@@ -134,9 +227,29 @@ export class ChatInterfaceComponent implements OnInit, OnDestroy {
|
|
|
134
227
|
*/
|
|
135
228
|
clearChat(): void {
|
|
136
229
|
if (confirm('确定要清空聊天记录吗?')) {
|
|
230
|
+
// 删除当前会话
|
|
231
|
+
if (this.currentSessionId) {
|
|
232
|
+
this.chatHistory.deleteSession(this.currentSessionId);
|
|
233
|
+
}
|
|
234
|
+
// 创建新会话
|
|
235
|
+
this.currentSessionId = this.generateSessionId();
|
|
137
236
|
this.messages = [];
|
|
138
237
|
this.sendWelcomeMessage();
|
|
139
|
-
|
|
238
|
+
|
|
239
|
+
// 确保重置加载状态
|
|
240
|
+
this.isLoading = false;
|
|
241
|
+
|
|
242
|
+
// 延迟滚动和恢复焦点,确保DOM已更新
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
this.scrollToBottom();
|
|
245
|
+
// 尝试恢复输入框焦点
|
|
246
|
+
const inputElement = document.querySelector('.chat-textarea') as HTMLTextAreaElement;
|
|
247
|
+
if (inputElement) {
|
|
248
|
+
inputElement.focus();
|
|
249
|
+
}
|
|
250
|
+
}, 100);
|
|
251
|
+
|
|
252
|
+
this.logger.info('Chat cleared, new session created', { sessionId: this.currentSessionId });
|
|
140
253
|
}
|
|
141
254
|
}
|
|
142
255
|
|
|
@@ -162,34 +275,136 @@ export class ChatInterfaceComponent implements OnInit, OnDestroy {
|
|
|
162
275
|
/**
|
|
163
276
|
* 切换提供商
|
|
164
277
|
*/
|
|
165
|
-
switchProvider(): void {
|
|
166
|
-
//
|
|
167
|
-
this.
|
|
278
|
+
async switchProvider(): Promise<void> {
|
|
279
|
+
// 从配置服务获取已配置的提供商
|
|
280
|
+
const allConfigs = this.config.getAllProviderConfigs();
|
|
281
|
+
const configuredProviders = Object.keys(allConfigs)
|
|
282
|
+
.filter(key => allConfigs[key] && allConfigs[key].enabled !== false)
|
|
283
|
+
.map(key => ({
|
|
284
|
+
name: key,
|
|
285
|
+
displayName: allConfigs[key].displayName || key
|
|
286
|
+
}));
|
|
287
|
+
|
|
288
|
+
if (configuredProviders.length === 0) {
|
|
289
|
+
alert('没有可用的AI提供商,请先在设置中配置。');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 构建提供商列表
|
|
294
|
+
const providerList = configuredProviders.map((p, i) =>
|
|
295
|
+
`${i + 1}. ${p.displayName}`
|
|
296
|
+
).join('\n');
|
|
297
|
+
|
|
298
|
+
const choice = prompt(
|
|
299
|
+
`当前使用: ${this.currentProvider}\n\n可用的AI提供商:\n${providerList}\n\n请输入序号选择提供商:`,
|
|
300
|
+
'1'
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (choice) {
|
|
304
|
+
const index = parseInt(choice, 10) - 1;
|
|
305
|
+
if (index >= 0 && index < configuredProviders.length) {
|
|
306
|
+
const selectedProvider = configuredProviders[index];
|
|
307
|
+
this.config.setDefaultProvider(selectedProvider.name);
|
|
308
|
+
this.currentProvider = selectedProvider.displayName;
|
|
309
|
+
this.logger.info('Provider switched', { provider: selectedProvider.name });
|
|
310
|
+
|
|
311
|
+
// 添加系统消息
|
|
312
|
+
const systemMessage: ChatMessage = {
|
|
313
|
+
id: this.generateId(),
|
|
314
|
+
role: MessageRole.SYSTEM,
|
|
315
|
+
content: `已切换到 ${this.currentProvider}`,
|
|
316
|
+
timestamp: new Date()
|
|
317
|
+
};
|
|
318
|
+
this.messages.push(systemMessage);
|
|
319
|
+
} else {
|
|
320
|
+
alert('无效的选择');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 滚动到底部(公开方法)
|
|
327
|
+
*/
|
|
328
|
+
scrollToBottom(): void {
|
|
329
|
+
this.shouldScrollToBottom = true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 滚动到顶部
|
|
334
|
+
*/
|
|
335
|
+
scrollToTop(): void {
|
|
336
|
+
const chatContainer = this.chatContainerRef?.nativeElement || document.querySelector('.ai-chat-container');
|
|
337
|
+
if (chatContainer) {
|
|
338
|
+
chatContainer.scrollTo({ top: 0, behavior: 'smooth' });
|
|
339
|
+
}
|
|
168
340
|
}
|
|
169
341
|
|
|
170
342
|
/**
|
|
171
|
-
*
|
|
343
|
+
* 实际执行滚动到底部
|
|
172
344
|
*/
|
|
173
|
-
private
|
|
174
|
-
const chatContainer = document.querySelector('.ai-chat-container');
|
|
345
|
+
private performScrollToBottom(): void {
|
|
346
|
+
const chatContainer = this.chatContainerRef?.nativeElement || document.querySelector('.ai-chat-container');
|
|
175
347
|
if (chatContainer) {
|
|
176
|
-
chatContainer.
|
|
348
|
+
chatContainer.scrollTo({ top: chatContainer.scrollHeight, behavior: 'smooth' });
|
|
177
349
|
}
|
|
178
350
|
}
|
|
179
351
|
|
|
352
|
+
/**
|
|
353
|
+
* 处理滚动事件
|
|
354
|
+
*/
|
|
355
|
+
onScroll(event: Event): void {
|
|
356
|
+
const target = event.target as HTMLElement;
|
|
357
|
+
if (!target) return;
|
|
358
|
+
this.updateScrollButtons(target);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 检查滚动状态(初始化时调用)
|
|
363
|
+
*/
|
|
364
|
+
private checkScrollState(): void {
|
|
365
|
+
const chatContainer = this.chatContainerRef?.nativeElement || document.querySelector('.ai-chat-container');
|
|
366
|
+
if (chatContainer) {
|
|
367
|
+
this.updateScrollButtons(chatContainer);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 更新滚动按钮显示状态
|
|
373
|
+
*/
|
|
374
|
+
private updateScrollButtons(container: HTMLElement): void {
|
|
375
|
+
const scrollTop = container.scrollTop;
|
|
376
|
+
const scrollHeight = container.scrollHeight;
|
|
377
|
+
const clientHeight = container.clientHeight;
|
|
378
|
+
|
|
379
|
+
// 判断是否显示滚动按钮
|
|
380
|
+
this.showScrollTop = scrollTop > 50;
|
|
381
|
+
this.showScrollBottom = scrollHeight > clientHeight && scrollTop < scrollHeight - clientHeight - 50;
|
|
382
|
+
}
|
|
383
|
+
|
|
180
384
|
/**
|
|
181
385
|
* 保存聊天历史
|
|
182
386
|
*/
|
|
183
387
|
private saveChatHistory(): void {
|
|
184
388
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
389
|
+
if (this.messages.length > 0 && this.currentSessionId) {
|
|
390
|
+
this.chatHistory.saveSession(this.currentSessionId, this.messages);
|
|
391
|
+
this.logger.info('Chat history saved', {
|
|
392
|
+
sessionId: this.currentSessionId,
|
|
393
|
+
messageCount: this.messages.length
|
|
394
|
+
});
|
|
395
|
+
}
|
|
188
396
|
} catch (error) {
|
|
189
397
|
this.logger.error('Failed to save chat history', error);
|
|
190
398
|
}
|
|
191
399
|
}
|
|
192
400
|
|
|
401
|
+
/**
|
|
402
|
+
* 生成会话 ID
|
|
403
|
+
*/
|
|
404
|
+
private generateSessionId(): string {
|
|
405
|
+
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
193
408
|
/**
|
|
194
409
|
* 生成唯一ID
|
|
195
410
|
*/
|
|
@@ -222,3 +437,4 @@ export class ChatInterfaceComponent implements OnInit, OnDestroy {
|
|
|
222
437
|
return date1.toDateString() === date2.toDateString();
|
|
223
438
|
}
|
|
224
439
|
}
|
|
440
|
+
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
color: white;
|
|
131
131
|
|
|
132
132
|
&:hover {
|
|
133
|
-
background-color:
|
|
133
|
+
background-color: #5a6268;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
color: white;
|
|
140
140
|
|
|
141
141
|
&:hover {
|
|
142
|
-
background-color:
|
|
142
|
+
background-color: #c82333;
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
color: var(--ai-dark);
|
|
149
149
|
|
|
150
150
|
&:hover {
|
|
151
|
-
background-color:
|
|
151
|
+
background-color: #e0a800;
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
}
|
|
@@ -169,4 +169,4 @@
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
}
|
|
172
|
+
}
|
|
@@ -8,7 +8,18 @@ import { LoggerService } from '../../services/core/logger.service';
|
|
|
8
8
|
styleUrls: ['./chat-settings.component.scss']
|
|
9
9
|
})
|
|
10
10
|
export class ChatSettingsComponent implements OnInit {
|
|
11
|
-
settings
|
|
11
|
+
settings: {
|
|
12
|
+
chatHistoryEnabled: boolean;
|
|
13
|
+
maxChatHistory: number;
|
|
14
|
+
autoSaveChat: boolean;
|
|
15
|
+
theme: string;
|
|
16
|
+
fontSize: number;
|
|
17
|
+
compactMode: boolean;
|
|
18
|
+
showTimestamps: boolean;
|
|
19
|
+
showAvatars: boolean;
|
|
20
|
+
enterToSend: boolean;
|
|
21
|
+
soundEnabled: boolean;
|
|
22
|
+
} = {
|
|
12
23
|
chatHistoryEnabled: true,
|
|
13
24
|
maxChatHistory: 100,
|
|
14
25
|
autoSaveChat: true,
|
|
@@ -42,16 +53,16 @@ export class ChatSettingsComponent implements OnInit {
|
|
|
42
53
|
* 加载设置
|
|
43
54
|
*/
|
|
44
55
|
private loadSettings(): void {
|
|
45
|
-
this.settings.chatHistoryEnabled = this.config.get('chatHistoryEnabled', true);
|
|
46
|
-
this.settings.maxChatHistory = this.config.get('maxChatHistory', 100);
|
|
47
|
-
this.settings.autoSaveChat = this.config.get('autoSaveChat', true);
|
|
48
|
-
this.settings.theme = this.config.get('theme', 'auto');
|
|
49
|
-
this.settings.fontSize = this.config.get('ui.fontSize', 14);
|
|
50
|
-
this.settings.compactMode = this.config.get('ui.compactMode', false);
|
|
51
|
-
this.settings.showTimestamps = this.config.get('ui.showTimestamps', true);
|
|
52
|
-
this.settings.showAvatars = this.config.get('ui.showAvatars', true);
|
|
53
|
-
this.settings.enterToSend = this.config.get('ui.enterToSend', true);
|
|
54
|
-
this.settings.soundEnabled = this.config.get('ui.soundEnabled', true);
|
|
56
|
+
this.settings.chatHistoryEnabled = this.config.get('chatHistoryEnabled', true) ?? true;
|
|
57
|
+
this.settings.maxChatHistory = this.config.get('maxChatHistory', 100) ?? 100;
|
|
58
|
+
this.settings.autoSaveChat = this.config.get('autoSaveChat', true) ?? true;
|
|
59
|
+
this.settings.theme = this.config.get('theme', 'auto') ?? 'auto';
|
|
60
|
+
this.settings.fontSize = this.config.get('ui.fontSize', 14) ?? 14;
|
|
61
|
+
this.settings.compactMode = this.config.get('ui.compactMode', false) ?? false;
|
|
62
|
+
this.settings.showTimestamps = this.config.get('ui.showTimestamps', true) ?? true;
|
|
63
|
+
this.settings.showAvatars = this.config.get('ui.showAvatars', true) ?? true;
|
|
64
|
+
this.settings.enterToSend = this.config.get('ui.enterToSend', true) ?? true;
|
|
65
|
+
this.settings.soundEnabled = this.config.get('ui.soundEnabled', true) ?? true;
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div class="error-message" [ngClass]="type">
|
|
2
|
+
<div class="error-icon">
|
|
3
|
+
<i [class]="getIconClass()"></i>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="error-content">
|
|
6
|
+
<h4 *ngIf="title">{{ title }}</h4>
|
|
7
|
+
<p>{{ message }}</p>
|
|
8
|
+
<div *ngIf="details" class="error-details">
|
|
9
|
+
<small>{{ details }}</small>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<button *ngIf="dismissible" class="error-close" (click)="onDismiss()">
|
|
13
|
+
<i class="icon-close"></i>
|
|
14
|
+
</button>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
.error-message {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: flex-start;
|
|
4
|
+
gap: 0.75rem;
|
|
5
|
+
padding: 1rem;
|
|
6
|
+
border-radius: 0.5rem;
|
|
7
|
+
border: 1px solid;
|
|
8
|
+
margin: 0.5rem 0;
|
|
9
|
+
|
|
10
|
+
&.error {
|
|
11
|
+
background-color: rgba(220, 53, 69, 0.1);
|
|
12
|
+
border-color: var(--ai-danger);
|
|
13
|
+
color: var(--ai-danger);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&.warning {
|
|
17
|
+
background-color: rgba(255, 193, 7, 0.1);
|
|
18
|
+
border-color: var(--ai-warning);
|
|
19
|
+
color: #856404;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&.info {
|
|
23
|
+
background-color: rgba(23, 162, 184, 0.1);
|
|
24
|
+
border-color: var(--ai-info);
|
|
25
|
+
color: var(--ai-info);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&.success {
|
|
29
|
+
background-color: rgba(40, 167, 69, 0.1);
|
|
30
|
+
border-color: var(--ai-success);
|
|
31
|
+
color: var(--ai-success);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.error-icon {
|
|
36
|
+
flex-shrink: 0;
|
|
37
|
+
font-size: 1.25rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.error-content {
|
|
41
|
+
flex: 1;
|
|
42
|
+
|
|
43
|
+
h4 {
|
|
44
|
+
margin: 0 0 0.5rem 0;
|
|
45
|
+
font-size: 1rem;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
p {
|
|
50
|
+
margin: 0;
|
|
51
|
+
line-height: 1.5;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.error-details {
|
|
55
|
+
margin-top: 0.5rem;
|
|
56
|
+
opacity: 0.8;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.error-close {
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
width: 24px;
|
|
63
|
+
height: 24px;
|
|
64
|
+
border: none;
|
|
65
|
+
background: transparent;
|
|
66
|
+
border-radius: 0.25rem;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
opacity: 0.6;
|
|
72
|
+
transition: opacity 0.2s;
|
|
73
|
+
|
|
74
|
+
&:hover {
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -2,102 +2,8 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
|
2
2
|
|
|
3
3
|
@Component({
|
|
4
4
|
selector: 'app-error-message',
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<div class="error-icon">
|
|
8
|
-
<i [class]="getIconClass()"></i>
|
|
9
|
-
</div>
|
|
10
|
-
<div class="error-content">
|
|
11
|
-
<h4 *ngIf="title">{{ title }}</h4>
|
|
12
|
-
<p>{{ message }}</p>
|
|
13
|
-
<div *ngIf="details" class="error-details">
|
|
14
|
-
<small>{{ details }}</small>
|
|
15
|
-
</div>
|
|
16
|
-
</div>
|
|
17
|
-
<button *ngIf="dismissible" class="error-close" (click)="onDismiss()">
|
|
18
|
-
<i class="icon-close"></i>
|
|
19
|
-
</button>
|
|
20
|
-
</div>
|
|
21
|
-
`,
|
|
22
|
-
styles: [`
|
|
23
|
-
.error-message {
|
|
24
|
-
display: flex;
|
|
25
|
-
align-items: flex-start;
|
|
26
|
-
gap: 0.75rem;
|
|
27
|
-
padding: 1rem;
|
|
28
|
-
border-radius: 0.5rem;
|
|
29
|
-
border: 1px solid;
|
|
30
|
-
margin: 0.5rem 0;
|
|
31
|
-
|
|
32
|
-
&.error {
|
|
33
|
-
background-color: rgba(220, 53, 69, 0.1);
|
|
34
|
-
border-color: var(--ai-danger);
|
|
35
|
-
color: var(--ai-danger);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
&.warning {
|
|
39
|
-
background-color: rgba(255, 193, 7, 0.1);
|
|
40
|
-
border-color: var(--ai-warning);
|
|
41
|
-
color: #856404;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
&.info {
|
|
45
|
-
background-color: rgba(23, 162, 184, 0.1);
|
|
46
|
-
border-color: var(--ai-info);
|
|
47
|
-
color: var(--ai-info);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
&.success {
|
|
51
|
-
background-color: rgba(40, 167, 69, 0.1);
|
|
52
|
-
border-color: var(--ai-success);
|
|
53
|
-
color: var(--ai-success);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.error-icon {
|
|
58
|
-
flex-shrink: 0;
|
|
59
|
-
font-size: 1.25rem;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.error-content {
|
|
63
|
-
flex: 1;
|
|
64
|
-
|
|
65
|
-
h4 {
|
|
66
|
-
margin: 0 0 0.5rem 0;
|
|
67
|
-
font-size: 1rem;
|
|
68
|
-
font-weight: 600;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
p {
|
|
72
|
-
margin: 0;
|
|
73
|
-
line-height: 1.5;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.error-details {
|
|
77
|
-
margin-top: 0.5rem;
|
|
78
|
-
opacity: 0.8;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.error-close {
|
|
83
|
-
flex-shrink: 0;
|
|
84
|
-
width: 24px;
|
|
85
|
-
height: 24px;
|
|
86
|
-
border: none;
|
|
87
|
-
background: transparent;
|
|
88
|
-
border-radius: 0.25rem;
|
|
89
|
-
display: flex;
|
|
90
|
-
align-items: center;
|
|
91
|
-
justify-content: center;
|
|
92
|
-
cursor: pointer;
|
|
93
|
-
opacity: 0.6;
|
|
94
|
-
transition: opacity 0.2s;
|
|
95
|
-
|
|
96
|
-
&:hover {
|
|
97
|
-
opacity: 1;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
`]
|
|
5
|
+
templateUrl: './error-message.component.html',
|
|
6
|
+
styleUrls: ['./error-message.component.scss']
|
|
101
7
|
})
|
|
102
8
|
export class ErrorMessageComponent {
|
|
103
9
|
@Input() type: 'error' | 'warning' | 'info' | 'success' = 'error';
|