tabby-ai-assistant 1.0.8 → 1.0.9
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 +16 -3
- package/dist/components/chat/chat-input.component.d.ts +4 -0
- package/dist/components/chat/chat-interface.component.d.ts +22 -1
- package/dist/components/chat/chat-settings.component.d.ts +21 -11
- package/dist/components/settings/ai-settings-tab.component.d.ts +14 -4
- package/dist/components/settings/general-settings.component.d.ts +43 -12
- package/dist/components/settings/provider-config.component.d.ts +83 -5
- package/dist/components/settings/security-settings.component.d.ts +14 -4
- package/dist/i18n/index.d.ts +48 -0
- package/dist/i18n/translations/en-US.d.ts +5 -0
- package/dist/i18n/translations/ja-JP.d.ts +5 -0
- package/dist/i18n/translations/zh-CN.d.ts +5 -0
- package/dist/i18n/types.d.ts +198 -0
- package/dist/index.js +1 -1
- package/dist/services/chat/ai-sidebar.service.d.ts +23 -1
- package/dist/services/core/theme.service.d.ts +53 -0
- package/package.json +1 -1
- package/src/components/chat/ai-sidebar.component.scss +468 -0
- package/src/components/chat/ai-sidebar.component.ts +47 -344
- package/src/components/chat/chat-input.component.scss +2 -2
- package/src/components/chat/chat-input.component.ts +16 -5
- package/src/components/chat/chat-interface.component.html +11 -11
- package/src/components/chat/chat-interface.component.scss +410 -4
- package/src/components/chat/chat-interface.component.ts +105 -14
- package/src/components/chat/chat-message.component.scss +3 -3
- package/src/components/chat/chat-message.component.ts +3 -2
- package/src/components/chat/chat-settings.component.html +95 -61
- package/src/components/chat/chat-settings.component.scss +224 -50
- package/src/components/chat/chat-settings.component.ts +56 -30
- package/src/components/security/risk-confirm-dialog.component.scss +7 -7
- package/src/components/settings/ai-settings-tab.component.html +27 -27
- package/src/components/settings/ai-settings-tab.component.scss +34 -20
- package/src/components/settings/ai-settings-tab.component.ts +59 -20
- package/src/components/settings/general-settings.component.html +69 -40
- package/src/components/settings/general-settings.component.scss +151 -58
- package/src/components/settings/general-settings.component.ts +168 -55
- package/src/components/settings/provider-config.component.html +149 -60
- package/src/components/settings/provider-config.component.scss +273 -153
- package/src/components/settings/provider-config.component.ts +177 -19
- package/src/components/settings/security-settings.component.html +70 -39
- package/src/components/settings/security-settings.component.scss +104 -8
- package/src/components/settings/security-settings.component.ts +48 -10
- package/src/i18n/index.ts +129 -0
- package/src/i18n/translations/en-US.ts +193 -0
- package/src/i18n/translations/ja-JP.ts +193 -0
- package/src/i18n/translations/zh-CN.ts +193 -0
- package/src/i18n/types.ts +224 -0
- package/src/index.ts +6 -0
- package/src/services/chat/ai-sidebar.service.ts +157 -5
- package/src/services/core/theme.service.ts +480 -0
- package/src/styles/ai-assistant.scss +8 -88
- package/src/styles/themes.scss +161 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
|
1
|
+
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import { takeUntil } from 'rxjs/operators';
|
|
2
4
|
import { ConfigProviderService } from '../../services/core/config-provider.service';
|
|
3
5
|
import { LoggerService } from '../../services/core/logger.service';
|
|
6
|
+
import { TranslateService } from '../../i18n';
|
|
4
7
|
|
|
5
8
|
@Component({
|
|
6
9
|
selector: 'app-provider-config',
|
|
7
10
|
templateUrl: './provider-config.component.html',
|
|
8
|
-
styleUrls: ['./provider-config.component.scss']
|
|
11
|
+
styleUrls: ['./provider-config.component.scss'],
|
|
12
|
+
encapsulation: ViewEncapsulation.None
|
|
9
13
|
})
|
|
10
|
-
export class ProviderConfigComponent implements OnInit {
|
|
14
|
+
export class ProviderConfigComponent implements OnInit, OnDestroy {
|
|
11
15
|
@Input() providerStatus: any = {};
|
|
12
16
|
@Output() refreshStatus = new EventEmitter<void>();
|
|
13
17
|
@Output() switchProvider = new EventEmitter<string>();
|
|
@@ -17,12 +21,20 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
17
21
|
|
|
18
22
|
selectedProvider = '';
|
|
19
23
|
configs: { [key: string]: any } = {};
|
|
24
|
+
expandedProvider: string = '';
|
|
25
|
+
localStatus: { [key: string]: boolean } = {};
|
|
20
26
|
|
|
21
|
-
//
|
|
22
|
-
|
|
27
|
+
// 翻译对象
|
|
28
|
+
t: any;
|
|
29
|
+
|
|
30
|
+
private destroy$ = new Subject<void>();
|
|
31
|
+
|
|
32
|
+
// 云端提供商模板
|
|
33
|
+
cloudProviderTemplates = {
|
|
23
34
|
'openai': {
|
|
24
35
|
name: 'OpenAI',
|
|
25
36
|
description: 'OpenAI GPT模型',
|
|
37
|
+
icon: 'fa-robot',
|
|
26
38
|
fields: [
|
|
27
39
|
{ key: 'apiKey', label: 'API Key', type: 'password', required: true },
|
|
28
40
|
{ key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.openai.com/v1', required: false },
|
|
@@ -32,6 +44,7 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
32
44
|
'anthropic': {
|
|
33
45
|
name: 'Anthropic Claude',
|
|
34
46
|
description: 'Anthropic Claude模型',
|
|
47
|
+
icon: 'fa-comments',
|
|
35
48
|
fields: [
|
|
36
49
|
{ key: 'apiKey', label: 'API Key', type: 'password', required: true },
|
|
37
50
|
{ key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.anthropic.com', required: false },
|
|
@@ -41,6 +54,7 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
41
54
|
'minimax': {
|
|
42
55
|
name: 'Minimax',
|
|
43
56
|
description: 'Minimax AI模型',
|
|
57
|
+
icon: 'fa-brain',
|
|
44
58
|
fields: [
|
|
45
59
|
{ key: 'apiKey', label: 'API Key', type: 'password', required: true },
|
|
46
60
|
{ key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.minimaxi.com/anthropic', required: false },
|
|
@@ -50,6 +64,7 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
50
64
|
'glm': {
|
|
51
65
|
name: 'GLM (ChatGLM)',
|
|
52
66
|
description: '智谱AI ChatGLM模型',
|
|
67
|
+
icon: 'fa-network-wired',
|
|
53
68
|
fields: [
|
|
54
69
|
{ key: 'apiKey', label: 'API Key', type: 'password', required: true },
|
|
55
70
|
{ key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://open.bigmodel.cn/api/paas/v4', required: false },
|
|
@@ -58,13 +73,55 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
58
73
|
}
|
|
59
74
|
};
|
|
60
75
|
|
|
76
|
+
// 本地提供商模板(不需要 API Key)
|
|
77
|
+
localProviderTemplates = {
|
|
78
|
+
'ollama': {
|
|
79
|
+
name: 'Ollama (本地)',
|
|
80
|
+
description: '本地运行的 Ollama 服务,支持 Llama、Qwen 等模型',
|
|
81
|
+
icon: 'fa-server',
|
|
82
|
+
defaultURL: 'http://localhost:11434/v1',
|
|
83
|
+
fields: [
|
|
84
|
+
{ key: 'baseURL', label: 'Base URL', type: 'text', default: 'http://localhost:11434/v1', required: true, placeholder: '例如: http://localhost:11434/v1' },
|
|
85
|
+
{ key: 'model', label: 'Model', type: 'text', default: 'llama3.1', required: false, placeholder: '例如: llama3.1, qwen2.5, mistral' }
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
'vllm': {
|
|
89
|
+
name: 'vLLM (本地)',
|
|
90
|
+
description: '本地运行的 vLLM 服务,适合生产部署',
|
|
91
|
+
icon: 'fa-database',
|
|
92
|
+
defaultURL: 'http://localhost:8000/v1',
|
|
93
|
+
fields: [
|
|
94
|
+
{ key: 'baseURL', label: 'Base URL', type: 'text', default: 'http://localhost:8000/v1', required: true, placeholder: '例如: http://localhost:8000/v1' },
|
|
95
|
+
{ key: 'apiKey', label: 'API Key (可选)', type: 'password', required: false },
|
|
96
|
+
{ key: 'model', label: 'Model', type: 'text', default: 'meta-llama/Llama-3.1-8B', required: false, placeholder: 'HuggingFace 模型路径' }
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
61
101
|
constructor(
|
|
62
102
|
private config: ConfigProviderService,
|
|
63
|
-
private logger: LoggerService
|
|
64
|
-
|
|
103
|
+
private logger: LoggerService,
|
|
104
|
+
private translate: TranslateService
|
|
105
|
+
) {
|
|
106
|
+
this.t = this.translate.t;
|
|
107
|
+
}
|
|
65
108
|
|
|
66
109
|
ngOnInit(): void {
|
|
110
|
+
// 监听语言变化
|
|
111
|
+
this.translate.translation$.pipe(
|
|
112
|
+
takeUntil(this.destroy$)
|
|
113
|
+
).subscribe(translation => {
|
|
114
|
+
this.t = translation;
|
|
115
|
+
});
|
|
116
|
+
|
|
67
117
|
this.loadConfigs();
|
|
118
|
+
// 检测本地供应商状态
|
|
119
|
+
this.checkLocalProviderStatus();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ngOnDestroy(): void {
|
|
123
|
+
this.destroy$.next();
|
|
124
|
+
this.destroy$.complete();
|
|
68
125
|
}
|
|
69
126
|
|
|
70
127
|
/**
|
|
@@ -76,6 +133,89 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
76
133
|
this.selectedProvider = this.config.getDefaultProvider();
|
|
77
134
|
}
|
|
78
135
|
|
|
136
|
+
/**
|
|
137
|
+
* 切换展开/折叠
|
|
138
|
+
*/
|
|
139
|
+
toggleExpand(providerName: string): void {
|
|
140
|
+
this.expandedProvider = this.expandedProvider === providerName ? '' : providerName;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 检查是否是本地提供商
|
|
145
|
+
*/
|
|
146
|
+
isLocalProvider(providerName: string): boolean {
|
|
147
|
+
return providerName in this.localProviderTemplates;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 检测本地供应商状态
|
|
152
|
+
*/
|
|
153
|
+
private async checkLocalProviderStatus(): Promise<void> {
|
|
154
|
+
const localUrls: { [key: string]: string } = {
|
|
155
|
+
'ollama': 'http://localhost:11434/v1/models',
|
|
156
|
+
'vllm': 'http://localhost:8000/v1/models'
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
for (const [name, url] of Object.entries(localUrls)) {
|
|
160
|
+
try {
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
setTimeout(() => controller.abort(), 2000);
|
|
163
|
+
|
|
164
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
165
|
+
this.localStatus[name] = response.ok;
|
|
166
|
+
} catch {
|
|
167
|
+
this.localStatus[name] = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 获取本地供应商在线状态
|
|
174
|
+
*/
|
|
175
|
+
getLocalStatus(providerName: string): { text: string; color: string; icon: string } {
|
|
176
|
+
const isOnline = this.localStatus[providerName];
|
|
177
|
+
return isOnline
|
|
178
|
+
? { text: '在线', color: '#4caf50', icon: 'fa-check-circle' }
|
|
179
|
+
: { text: '离线', color: '#f44336', icon: 'fa-times-circle' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 测试本地提供商连接
|
|
184
|
+
*/
|
|
185
|
+
async testLocalProvider(providerName: string): Promise<void> {
|
|
186
|
+
const template = this.localProviderTemplates[providerName];
|
|
187
|
+
const baseURL = this.configs[providerName]?.baseURL || template?.defaultURL;
|
|
188
|
+
|
|
189
|
+
if (!baseURL) {
|
|
190
|
+
alert(this.t.providers.baseURL + ': ' + this.t.providers.testError);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const testingMessage = `${this.t.providers.testConnection} ${template.name}...`;
|
|
195
|
+
this.logger.info(testingMessage);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const response = await fetch(`${baseURL}/models`, {
|
|
199
|
+
method: 'GET',
|
|
200
|
+
signal: AbortSignal.timeout(5000)
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (response.ok) {
|
|
204
|
+
alert(`✅ ${template.name}: ${this.t.providers.testSuccess}`);
|
|
205
|
+
this.localStatus[providerName] = true;
|
|
206
|
+
this.logger.info('Local provider test successful', { provider: providerName });
|
|
207
|
+
} else {
|
|
208
|
+
alert(`❌ ${this.t.providers.testFail}: ${response.status}`);
|
|
209
|
+
this.localStatus[providerName] = false;
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const errorMessage = error instanceof Error ? error.message : this.t.providers.testError;
|
|
213
|
+
alert(`❌ ${template.name}\n\n${this.t.providers.testError}\n${errorMessage}`);
|
|
214
|
+
this.localStatus[providerName] = false;
|
|
215
|
+
this.logger.error('Local provider test failed', { provider: providerName, error: errorMessage });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
79
219
|
/**
|
|
80
220
|
* 保存配置
|
|
81
221
|
*/
|
|
@@ -92,7 +232,11 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
92
232
|
*/
|
|
93
233
|
addProvider(providerName: string): void {
|
|
94
234
|
if (!this.configs[providerName]) {
|
|
95
|
-
|
|
235
|
+
// 检查是云端还是本地提供商
|
|
236
|
+
let template = this.cloudProviderTemplates[providerName];
|
|
237
|
+
if (!template) {
|
|
238
|
+
template = this.localProviderTemplates[providerName];
|
|
239
|
+
}
|
|
96
240
|
if (template) {
|
|
97
241
|
const newConfig = {
|
|
98
242
|
name: providerName,
|
|
@@ -110,7 +254,7 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
110
254
|
* 删除提供商
|
|
111
255
|
*/
|
|
112
256
|
removeProvider(providerName: string): void {
|
|
113
|
-
if (confirm(
|
|
257
|
+
if (confirm(this.t.providers.deleteConfirm)) {
|
|
114
258
|
delete this.configs[providerName];
|
|
115
259
|
this.config.deleteProviderConfig(providerName);
|
|
116
260
|
this.logger.info('Provider config removed', { provider: providerName });
|
|
@@ -133,7 +277,13 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
133
277
|
async testConnection(providerName: string): Promise<void> {
|
|
134
278
|
const providerConfig = this.configs[providerName];
|
|
135
279
|
if (!providerConfig) {
|
|
136
|
-
alert(
|
|
280
|
+
alert(this.t.providers.testError);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 本地提供商使用不同的测试方法
|
|
285
|
+
if (this.isLocalProvider(providerName)) {
|
|
286
|
+
await this.testLocalProvider(providerName);
|
|
137
287
|
return;
|
|
138
288
|
}
|
|
139
289
|
|
|
@@ -141,15 +291,15 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
141
291
|
const baseURL = providerConfig.baseURL;
|
|
142
292
|
|
|
143
293
|
if (!apiKey) {
|
|
144
|
-
alert('
|
|
294
|
+
alert(this.t.providers.apiKey + ': ' + this.t.providers.testError);
|
|
145
295
|
return;
|
|
146
296
|
}
|
|
147
297
|
|
|
148
|
-
const template =
|
|
298
|
+
const template = this.cloudProviderTemplates[providerName];
|
|
149
299
|
const providerDisplayName = template?.name || providerName;
|
|
150
300
|
|
|
151
301
|
// 显示测试中状态
|
|
152
|
-
const testingMessage =
|
|
302
|
+
const testingMessage = `${this.t.providers.testConnection} ${providerDisplayName}...`;
|
|
153
303
|
this.logger.info(testingMessage);
|
|
154
304
|
|
|
155
305
|
try {
|
|
@@ -164,16 +314,16 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
164
314
|
});
|
|
165
315
|
|
|
166
316
|
if (response.ok) {
|
|
167
|
-
alert(`✅ ${
|
|
317
|
+
alert(`✅ ${this.t.providers.testSuccess}`);
|
|
168
318
|
this.logger.info('Connection test successful', { provider: providerName });
|
|
169
319
|
} else {
|
|
170
320
|
const errorData = await response.text();
|
|
171
|
-
alert(`❌ ${
|
|
321
|
+
alert(`❌ ${this.t.providers.testFail}\n\nStatus: ${response.status}\n${errorData.substring(0, 200)}`);
|
|
172
322
|
this.logger.error('Connection test failed', { provider: providerName, status: response.status });
|
|
173
323
|
}
|
|
174
324
|
} catch (error) {
|
|
175
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
176
|
-
alert(`❌ ${
|
|
325
|
+
const errorMessage = error instanceof Error ? error.message : this.t.providers.testError;
|
|
326
|
+
alert(`❌ ${this.t.providers.testFail}\n\n${errorMessage}`);
|
|
177
327
|
this.logger.error('Connection test error', { provider: providerName, error: errorMessage });
|
|
178
328
|
}
|
|
179
329
|
}
|
|
@@ -286,10 +436,18 @@ export class ProviderConfigComponent implements OnInit {
|
|
|
286
436
|
}
|
|
287
437
|
|
|
288
438
|
/**
|
|
289
|
-
*
|
|
439
|
+
* 获取提供商模板(支持云端和本地)
|
|
290
440
|
*/
|
|
291
441
|
getProviderTemplate(providerName: string): any {
|
|
292
|
-
return this.
|
|
442
|
+
return this.cloudProviderTemplates[providerName] || this.localProviderTemplates[providerName];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 获取提供商图标
|
|
447
|
+
*/
|
|
448
|
+
getProviderIcon(providerName: string): string {
|
|
449
|
+
const template = this.getProviderTemplate(providerName);
|
|
450
|
+
return template?.icon || 'fa-cog';
|
|
293
451
|
}
|
|
294
452
|
|
|
295
453
|
/**
|
|
@@ -1,51 +1,82 @@
|
|
|
1
1
|
<div class="security-settings">
|
|
2
|
-
<h3>
|
|
2
|
+
<h3>{{ t.security.title }}</h3>
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
<!-- 密码保护 -->
|
|
5
|
+
<div class="settings-section">
|
|
6
|
+
<h4>{{ t.security.accessControl }}</h4>
|
|
7
|
+
<div class="form-group">
|
|
8
|
+
<label class="form-label">
|
|
9
|
+
<input type="checkbox" [(ngModel)]="settings.enablePasswordProtection">
|
|
10
|
+
{{ t.security.passwordProtection }}
|
|
11
|
+
</label>
|
|
12
|
+
<small class="form-text text-muted">
|
|
13
|
+
{{ t.security.passwordProtectionDesc }}
|
|
14
|
+
</small>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="form-group" *ngIf="settings.enablePasswordProtection">
|
|
17
|
+
<label class="form-label">{{ t.security.setPassword }}</label>
|
|
18
|
+
<input type="password" class="form-control" [(ngModel)]="password" [placeholder]="t.security.passwordPlaceholder">
|
|
19
|
+
</div>
|
|
12
20
|
</div>
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
22
|
+
<!-- 风险评估 -->
|
|
23
|
+
<div class="settings-section">
|
|
24
|
+
<h4>{{ t.security.riskAssessment }}</h4>
|
|
25
|
+
<div class="form-group">
|
|
26
|
+
<label class="form-label">
|
|
27
|
+
<input type="checkbox" [(ngModel)]="settings.enableRiskAssessment">
|
|
28
|
+
{{ t.security.riskAssessment }}
|
|
29
|
+
</label>
|
|
30
|
+
<small class="form-text text-muted">
|
|
31
|
+
{{ t.security.riskAssessmentDesc }}
|
|
32
|
+
</small>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="form-group">
|
|
35
|
+
<label class="form-label">{{ t.security.defaultRiskLevel }}</label>
|
|
36
|
+
<select class="form-control" [(ngModel)]="settings.defaultRiskLevel">
|
|
37
|
+
<option value="low">{{ t.security.riskLow }}</option>
|
|
38
|
+
<option value="medium">{{ t.security.riskMedium }}</option>
|
|
39
|
+
<option value="high">{{ t.security.riskHigh }}</option>
|
|
40
|
+
</select>
|
|
41
|
+
</div>
|
|
17
42
|
</div>
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
<!-- 用户授权 -->
|
|
45
|
+
<div class="settings-section">
|
|
46
|
+
<h4>{{ t.security.userConsent }}</h4>
|
|
47
|
+
<div class="form-group">
|
|
48
|
+
<label class="form-label">
|
|
49
|
+
<input type="checkbox" [(ngModel)]="settings.enableConsentPersistence">
|
|
50
|
+
{{ t.security.rememberConsent }}
|
|
51
|
+
</label>
|
|
52
|
+
<small class="form-text text-muted">
|
|
53
|
+
{{ t.security.rememberConsentDesc }} {{ settings.consentExpiryDays }} {{ t.security.consentExpiryDays }}
|
|
54
|
+
</small>
|
|
55
|
+
</div>
|
|
27
56
|
</div>
|
|
28
57
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
<!-- 危险命令模式 -->
|
|
59
|
+
<div class="settings-section">
|
|
60
|
+
<h4>{{ t.security.dangerousPatterns }}</h4>
|
|
61
|
+
<div class="pattern-list">
|
|
62
|
+
<div *ngFor="let pattern of dangerousPatterns; let i = index" class="pattern-item">
|
|
63
|
+
<code>{{ pattern }}</code>
|
|
64
|
+
<button type="button" class="btn-icon" (click)="removeDangerousPattern(i)">
|
|
65
|
+
<i class="fa fa-times"></i>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="form-group add-pattern">
|
|
70
|
+
<input type="text" class="form-control" [(ngModel)]="newPattern" [placeholder]="t.security.patternPlaceholder">
|
|
71
|
+
<button type="button" class="btn btn-secondary" (click)="addDangerousPattern(newPattern)">
|
|
72
|
+
{{ t.security.addPattern }}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
36
75
|
</div>
|
|
37
76
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</label>
|
|
43
|
-
<small class="form-text text-muted">
|
|
44
|
-
Remember user consent for 30 days
|
|
45
|
-
</small>
|
|
77
|
+
<!-- 操作按钮 -->
|
|
78
|
+
<div class="settings-actions">
|
|
79
|
+
<button class="btn btn-primary" (click)="saveSettings()">{{ t.common.save }}</button>
|
|
80
|
+
<button class="btn btn-outline" (click)="resetToDefaults()">{{ t.security.resetDefaults }}</button>
|
|
46
81
|
</div>
|
|
47
|
-
|
|
48
|
-
<button class="btn btn-primary" (click)="saveSettings()">
|
|
49
|
-
Save Settings
|
|
50
|
-
</button>
|
|
51
82
|
</div>
|
|
@@ -2,17 +2,36 @@
|
|
|
2
2
|
.security-settings {
|
|
3
3
|
h3 {
|
|
4
4
|
margin-bottom: 1.5rem;
|
|
5
|
-
color:
|
|
5
|
+
color: var(--ai-text-primary);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
h4 {
|
|
9
|
+
font-size: 1.1rem;
|
|
10
|
+
margin-bottom: 1rem;
|
|
11
|
+
color: var(--ai-text-primary);
|
|
12
|
+
border-bottom: 1px solid var(--ai-border);
|
|
13
|
+
padding-bottom: 0.5rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.settings-section {
|
|
17
|
+
margin-bottom: 2rem;
|
|
18
|
+
padding-bottom: 1.5rem;
|
|
19
|
+
border-bottom: 1px solid var(--ai-border);
|
|
20
|
+
|
|
21
|
+
&:last-of-type {
|
|
22
|
+
border-bottom: none;
|
|
23
|
+
}
|
|
6
24
|
}
|
|
7
25
|
|
|
8
26
|
.form-group {
|
|
9
|
-
margin-bottom: 1.
|
|
27
|
+
margin-bottom: 1.25rem;
|
|
10
28
|
}
|
|
11
29
|
|
|
12
30
|
.form-label {
|
|
13
31
|
font-weight: 600;
|
|
14
32
|
margin-bottom: 0.5rem;
|
|
15
33
|
display: block;
|
|
34
|
+
color: var(--ai-text-primary);
|
|
16
35
|
|
|
17
36
|
input[type="checkbox"] {
|
|
18
37
|
margin-right: 0.5rem;
|
|
@@ -22,14 +41,16 @@
|
|
|
22
41
|
.form-control {
|
|
23
42
|
width: 100%;
|
|
24
43
|
padding: 0.5rem;
|
|
25
|
-
border: 1px solid
|
|
44
|
+
border: 1px solid var(--ai-border);
|
|
26
45
|
border-radius: 4px;
|
|
27
46
|
font-size: 14px;
|
|
47
|
+
background-color: var(--ai-bg-primary);
|
|
48
|
+
color: var(--ai-text-primary);
|
|
28
49
|
|
|
29
50
|
&:focus {
|
|
30
51
|
outline: none;
|
|
31
|
-
border-color:
|
|
32
|
-
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
|
52
|
+
border-color: var(--ai-primary);
|
|
53
|
+
box-shadow: 0 0 0 0.2rem rgba(var(--ai-primary-rgb, 0, 123, 255), 0.25);
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
56
|
|
|
@@ -37,7 +58,61 @@
|
|
|
37
58
|
display: block;
|
|
38
59
|
margin-top: 0.25rem;
|
|
39
60
|
font-size: 12px;
|
|
40
|
-
color:
|
|
61
|
+
color: var(--ai-text-secondary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* 危险命令模式列表 */
|
|
65
|
+
.pattern-list {
|
|
66
|
+
margin-bottom: 1rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.pattern-item {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
padding: 0.5rem 0.75rem;
|
|
74
|
+
background-color: var(--ai-bg-secondary);
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
margin-bottom: 0.5rem;
|
|
77
|
+
|
|
78
|
+
code {
|
|
79
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
80
|
+
color: var(--ai-danger);
|
|
81
|
+
font-size: 13px;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.btn-icon {
|
|
86
|
+
background: none;
|
|
87
|
+
border: none;
|
|
88
|
+
color: var(--ai-text-secondary);
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
padding: 0.25rem 0.5rem;
|
|
91
|
+
border-radius: 3px;
|
|
92
|
+
transition: color 0.2s, background-color 0.2s;
|
|
93
|
+
|
|
94
|
+
&:hover {
|
|
95
|
+
color: var(--ai-danger);
|
|
96
|
+
background-color: rgba(var(--ai-danger-rgb, 220, 53, 69), 0.1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.add-pattern {
|
|
101
|
+
display: flex;
|
|
102
|
+
gap: 0.5rem;
|
|
103
|
+
|
|
104
|
+
.form-control {
|
|
105
|
+
flex: 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* 操作按钮 */
|
|
110
|
+
.settings-actions {
|
|
111
|
+
display: flex;
|
|
112
|
+
gap: 1rem;
|
|
113
|
+
margin-top: 2rem;
|
|
114
|
+
padding-top: 1.5rem;
|
|
115
|
+
border-top: 1px solid var(--ai-border);
|
|
41
116
|
}
|
|
42
117
|
|
|
43
118
|
.btn {
|
|
@@ -50,11 +125,11 @@
|
|
|
50
125
|
transition: all 0.2s;
|
|
51
126
|
|
|
52
127
|
&.btn-primary {
|
|
53
|
-
background-color:
|
|
128
|
+
background-color: var(--ai-primary);
|
|
54
129
|
color: white;
|
|
55
130
|
|
|
56
131
|
&:hover {
|
|
57
|
-
background-color:
|
|
132
|
+
background-color: var(--ai-primary-hover);
|
|
58
133
|
transform: translateY(-1px);
|
|
59
134
|
}
|
|
60
135
|
|
|
@@ -62,5 +137,26 @@
|
|
|
62
137
|
transform: translateY(0);
|
|
63
138
|
}
|
|
64
139
|
}
|
|
140
|
+
|
|
141
|
+
&.btn-secondary {
|
|
142
|
+
background-color: var(--ai-secondary);
|
|
143
|
+
color: white;
|
|
144
|
+
|
|
145
|
+
&:hover {
|
|
146
|
+
background-color: var(--ai-secondary);
|
|
147
|
+
filter: brightness(0.9);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&.btn-outline {
|
|
152
|
+
background-color: transparent;
|
|
153
|
+
border: 1px solid var(--ai-border);
|
|
154
|
+
color: var(--ai-text-secondary);
|
|
155
|
+
|
|
156
|
+
&:hover {
|
|
157
|
+
background-color: var(--ai-bg-secondary);
|
|
158
|
+
color: var(--ai-text-primary);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
65
161
|
}
|
|
66
162
|
}
|