tabby-ai-assistant 1.0.8 → 1.0.10
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 +110 -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/dist/services/core/toast.service.d.ts +15 -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 +183 -60
- package/src/components/settings/provider-config.component.scss +332 -153
- package/src/components/settings/provider-config.component.ts +268 -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/services/core/toast.service.ts +36 -0
- package/src/styles/ai-assistant.scss +8 -88
- package/src/styles/themes.scss +161 -0
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
|
|
1
|
+
import { Component, Output, EventEmitter, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import { takeUntil } from 'rxjs/operators';
|
|
2
4
|
import { AiAssistantService } from '../../services/core/ai-assistant.service';
|
|
3
5
|
import { ConfigProviderService } from '../../services/core/config-provider.service';
|
|
4
6
|
import { LoggerService } from '../../services/core/logger.service';
|
|
7
|
+
import { ThemeService, ThemeType } from '../../services/core/theme.service';
|
|
5
8
|
import { ConfigService } from 'tabby-core';
|
|
9
|
+
import { TranslateService, SupportedLanguage } from '../../i18n';
|
|
6
10
|
|
|
7
11
|
@Component({
|
|
8
12
|
selector: 'app-general-settings',
|
|
9
13
|
templateUrl: './general-settings.component.html',
|
|
10
|
-
styleUrls: ['./general-settings.component.scss']
|
|
14
|
+
styleUrls: ['./general-settings.component.scss'],
|
|
15
|
+
encapsulation: ViewEncapsulation.None
|
|
11
16
|
})
|
|
12
|
-
export class GeneralSettingsComponent implements OnInit {
|
|
17
|
+
export class GeneralSettingsComponent implements OnInit, OnDestroy {
|
|
13
18
|
@Output() providerChanged = new EventEmitter<string>();
|
|
14
19
|
|
|
15
20
|
availableProviders: any[] = [];
|
|
@@ -18,16 +23,25 @@ export class GeneralSettingsComponent implements OnInit {
|
|
|
18
23
|
language: string = 'zh-CN';
|
|
19
24
|
theme: string = 'auto';
|
|
20
25
|
|
|
26
|
+
// 翻译对象
|
|
27
|
+
t: any;
|
|
28
|
+
|
|
29
|
+
// 本地供应商状态缓存
|
|
30
|
+
private localProviderStatus: { [key: string]: { text: string; color: string; icon: string; time: number } } = {};
|
|
31
|
+
private readonly statusCacheDuration = 30000; // 30秒缓存
|
|
32
|
+
private destroy$ = new Subject<void>();
|
|
33
|
+
|
|
21
34
|
languages = [
|
|
22
|
-
{ value: 'zh-CN', label: '简体中文' },
|
|
23
|
-
{ value: 'en-US', label: 'English' }
|
|
24
|
-
{ value: 'ja-JP', label: '日本語' }
|
|
35
|
+
{ value: 'zh-CN', label: '简体中文', flag: '🇨🇳' },
|
|
36
|
+
{ value: 'en-US', label: 'English', flag: '🇺🇸' }
|
|
25
37
|
];
|
|
26
38
|
|
|
27
39
|
themes = [
|
|
28
40
|
{ value: 'auto', label: '跟随系统' },
|
|
29
41
|
{ value: 'light', label: '浅色主题' },
|
|
30
|
-
{ value: 'dark', label: '深色主题' }
|
|
42
|
+
{ value: 'dark', label: '深色主题' },
|
|
43
|
+
{ value: 'pixel', label: '像素复古' },
|
|
44
|
+
{ value: 'tech', label: '赛博科技' }
|
|
31
45
|
];
|
|
32
46
|
|
|
33
47
|
// 提供商模板,用于显示名称
|
|
@@ -35,23 +49,57 @@ export class GeneralSettingsComponent implements OnInit {
|
|
|
35
49
|
'openai': 'OpenAI',
|
|
36
50
|
'anthropic': 'Anthropic Claude',
|
|
37
51
|
'minimax': 'Minimax',
|
|
38
|
-
'glm': 'GLM (ChatGLM)'
|
|
52
|
+
'glm': 'GLM (ChatGLM)',
|
|
53
|
+
'openai-compatible': 'OpenAI Compatible',
|
|
54
|
+
'ollama': 'Ollama (本地)',
|
|
55
|
+
'vllm': 'vLLM (本地)'
|
|
39
56
|
};
|
|
40
57
|
|
|
41
58
|
constructor(
|
|
42
59
|
private aiService: AiAssistantService,
|
|
43
60
|
private config: ConfigProviderService,
|
|
44
61
|
private tabbyConfig: ConfigService,
|
|
45
|
-
private logger: LoggerService
|
|
46
|
-
|
|
62
|
+
private logger: LoggerService,
|
|
63
|
+
private translate: TranslateService,
|
|
64
|
+
private themeService: ThemeService
|
|
65
|
+
) {
|
|
66
|
+
this.t = this.translate.t;
|
|
67
|
+
}
|
|
47
68
|
|
|
48
69
|
ngOnInit(): void {
|
|
70
|
+
// 监听语言变化
|
|
71
|
+
this.translate.translation$.pipe(
|
|
72
|
+
takeUntil(this.destroy$)
|
|
73
|
+
).subscribe(translation => {
|
|
74
|
+
this.t = translation;
|
|
75
|
+
// 更新主题翻译
|
|
76
|
+
this.updateThemeLabels();
|
|
77
|
+
});
|
|
78
|
+
|
|
49
79
|
this.loadSettings();
|
|
50
80
|
this.loadProviders();
|
|
51
81
|
// 应用当前主题
|
|
52
82
|
this.applyTheme(this.theme);
|
|
53
83
|
}
|
|
54
84
|
|
|
85
|
+
ngOnDestroy(): void {
|
|
86
|
+
this.destroy$.next();
|
|
87
|
+
this.destroy$.complete();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 更新主题标签翻译
|
|
92
|
+
*/
|
|
93
|
+
private updateThemeLabels(): void {
|
|
94
|
+
this.themes = [
|
|
95
|
+
{ value: 'auto', label: this.t.general.themeAuto },
|
|
96
|
+
{ value: 'light', label: this.t.general.themeLight },
|
|
97
|
+
{ value: 'dark', label: this.t.general.themeDark },
|
|
98
|
+
{ value: 'pixel', label: this.t.general.themePixel || '像素复古' },
|
|
99
|
+
{ value: 'tech', label: this.t.general.themeTech || '赛博科技' }
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
55
103
|
/**
|
|
56
104
|
* 加载设置
|
|
57
105
|
*/
|
|
@@ -63,22 +111,124 @@ export class GeneralSettingsComponent implements OnInit {
|
|
|
63
111
|
}
|
|
64
112
|
|
|
65
113
|
/**
|
|
66
|
-
* 加载可用提供商 -
|
|
114
|
+
* 加载可用提供商 - 支持云端和本地供应商
|
|
67
115
|
*/
|
|
68
116
|
private loadProviders(): void {
|
|
69
117
|
const allConfigs = this.config.getAllProviderConfigs();
|
|
118
|
+
|
|
119
|
+
// 本地供应商列表(不需要 API Key)
|
|
120
|
+
const localProviders = ['ollama', 'vllm'];
|
|
70
121
|
this.availableProviders = Object.keys(allConfigs)
|
|
71
|
-
.filter(key =>
|
|
122
|
+
.filter(key => {
|
|
123
|
+
const config = allConfigs[key];
|
|
124
|
+
if (!config) return false;
|
|
125
|
+
|
|
126
|
+
// 本地供应商:只需要有配置即可
|
|
127
|
+
if (localProviders.includes(key)) {
|
|
128
|
+
return config.enabled !== false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 云端供应商:需要 API Key
|
|
132
|
+
return !!config.apiKey;
|
|
133
|
+
})
|
|
72
134
|
.map(key => ({
|
|
73
135
|
name: key,
|
|
74
136
|
displayName: allConfigs[key].displayName || this.providerNames[key] || key,
|
|
75
|
-
description:
|
|
76
|
-
enabled: allConfigs[key].enabled !== false
|
|
137
|
+
description: this.getProviderDescription(key),
|
|
138
|
+
enabled: allConfigs[key].enabled !== false,
|
|
139
|
+
isLocal: localProviders.includes(key)
|
|
77
140
|
}));
|
|
78
141
|
|
|
79
142
|
this.logger.info('Loaded providers from config', { count: this.availableProviders.length });
|
|
80
143
|
}
|
|
81
144
|
|
|
145
|
+
/**
|
|
146
|
+
* 获取供应商描述
|
|
147
|
+
*/
|
|
148
|
+
private getProviderDescription(key: string): string {
|
|
149
|
+
const descriptions: { [key: string]: string } = {
|
|
150
|
+
'openai': '云端 OpenAI GPT 系列模型',
|
|
151
|
+
'anthropic': '云端 Anthropic Claude 系列模型',
|
|
152
|
+
'minimax': '云端 Minimax 大模型',
|
|
153
|
+
'glm': '云端 智谱 ChatGLM 模型',
|
|
154
|
+
'openai-compatible': '兼容 OpenAI API 的第三方服务',
|
|
155
|
+
'ollama': '本地运行的 Ollama 服务 (端口 11434)',
|
|
156
|
+
'vllm': '本地运行的 vLLM 服务 (端口 8000)'
|
|
157
|
+
};
|
|
158
|
+
return descriptions[key] || `${this.providerNames[key] || key} 提供商`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 获取云端提供商状态(同步返回)
|
|
163
|
+
*/
|
|
164
|
+
getProviderStatus(providerName: string): { text: string; color: string; icon: string } {
|
|
165
|
+
const providerConfig = this.config.getProviderConfig(providerName);
|
|
166
|
+
if (providerConfig && providerConfig.apiKey) {
|
|
167
|
+
return {
|
|
168
|
+
text: providerConfig.enabled !== false ? '已启用' : '已禁用',
|
|
169
|
+
color: providerConfig.enabled !== false ? '#4caf50' : '#ff9800',
|
|
170
|
+
icon: providerConfig.enabled !== false ? 'fa-check-circle' : 'fa-pause-circle'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { text: '未配置', color: '#9e9e9e', icon: 'fa-question-circle' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 检测本地供应商状态(异步)
|
|
178
|
+
*/
|
|
179
|
+
private async checkLocalProviderStatus(providerName: string): Promise<boolean> {
|
|
180
|
+
const urls: { [key: string]: string } = {
|
|
181
|
+
'ollama': 'http://localhost:11434/v1/models',
|
|
182
|
+
'vllm': 'http://localhost:8000/v1/models'
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
188
|
+
|
|
189
|
+
const response = await fetch(urls[providerName], {
|
|
190
|
+
method: 'GET',
|
|
191
|
+
signal: controller.signal
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
clearTimeout(timeoutId);
|
|
195
|
+
return response.ok;
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 获取本地供应商状态(同步返回,异步更新缓存)
|
|
203
|
+
*/
|
|
204
|
+
getLocalProviderStatus(providerName: string): { text: string; color: string; icon: string } {
|
|
205
|
+
const now = Date.now();
|
|
206
|
+
const cached = this.localProviderStatus[providerName];
|
|
207
|
+
|
|
208
|
+
// 检查缓存是否有效(30秒内)
|
|
209
|
+
if (cached && (now - cached.time) < this.statusCacheDuration) {
|
|
210
|
+
return { text: cached.text, color: cached.color, icon: cached.icon };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 返回默认状态并异步更新
|
|
214
|
+
const defaultStatus = { text: '检测中...', color: '#ff9800', icon: 'fa-spinner fa-spin' };
|
|
215
|
+
this.localProviderStatus[providerName] = { ...defaultStatus, time: now };
|
|
216
|
+
|
|
217
|
+
// 异步检查实际状态
|
|
218
|
+
this.checkLocalProviderStatus(providerName).then(isOnline => {
|
|
219
|
+
const status = isOnline
|
|
220
|
+
? { text: '在线', color: '#4caf50', icon: 'fa-check-circle', time: now }
|
|
221
|
+
: { text: '离线', color: '#f44336', icon: 'fa-times-circle', time: now };
|
|
222
|
+
this.localProviderStatus[providerName] = status;
|
|
223
|
+
this.logger.debug('Local provider status updated', { provider: providerName, isOnline });
|
|
224
|
+
}).catch(() => {
|
|
225
|
+
const status = { text: '离线', color: '#f44336', icon: 'fa-times-circle', time: now };
|
|
226
|
+
this.localProviderStatus[providerName] = status;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return defaultStatus;
|
|
230
|
+
}
|
|
231
|
+
|
|
82
232
|
/**
|
|
83
233
|
* 更新默认提供商
|
|
84
234
|
*/
|
|
@@ -103,7 +253,7 @@ export class GeneralSettingsComponent implements OnInit {
|
|
|
103
253
|
*/
|
|
104
254
|
updateLanguage(language: string): void {
|
|
105
255
|
this.language = language;
|
|
106
|
-
this.
|
|
256
|
+
this.translate.setLanguage(language as SupportedLanguage);
|
|
107
257
|
this.logger.info('Language updated', { language });
|
|
108
258
|
}
|
|
109
259
|
|
|
@@ -113,51 +263,14 @@ export class GeneralSettingsComponent implements OnInit {
|
|
|
113
263
|
updateTheme(theme: string): void {
|
|
114
264
|
this.theme = theme;
|
|
115
265
|
this.config.set('theme', theme);
|
|
116
|
-
this.applyTheme(theme);
|
|
266
|
+
this.themeService.applyTheme(theme as ThemeType);
|
|
117
267
|
this.logger.info('Theme updated', { theme });
|
|
118
268
|
}
|
|
119
269
|
|
|
120
270
|
/**
|
|
121
|
-
* 应用主题 -
|
|
271
|
+
* 应用主题 - 使用 ThemeService
|
|
122
272
|
*/
|
|
123
273
|
private applyTheme(theme: string): void {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// 移除现有主题类
|
|
127
|
-
root.classList.remove('ai-theme-light', 'ai-theme-dark');
|
|
128
|
-
document.body.classList.remove('ai-theme-light', 'ai-theme-dark');
|
|
129
|
-
|
|
130
|
-
if (theme === 'auto') {
|
|
131
|
-
// 跟随 Tabby 主题
|
|
132
|
-
const tabbyTheme = this.tabbyConfig.store?.appearance?.theme || 'dark';
|
|
133
|
-
const isDark = tabbyTheme.toLowerCase().includes('dark');
|
|
134
|
-
const themeClass = isDark ? 'ai-theme-dark' : 'ai-theme-light';
|
|
135
|
-
root.classList.add(themeClass);
|
|
136
|
-
document.body.classList.add(themeClass);
|
|
137
|
-
} else if (theme === 'light') {
|
|
138
|
-
root.classList.add('ai-theme-light');
|
|
139
|
-
document.body.classList.add('ai-theme-light');
|
|
140
|
-
} else {
|
|
141
|
-
root.classList.add('ai-theme-dark');
|
|
142
|
-
document.body.classList.add('ai-theme-dark');
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 获取提供商状态
|
|
148
|
-
*/
|
|
149
|
-
getProviderStatus(providerName: string): { text: string; color: string } {
|
|
150
|
-
const providerConfig = this.config.getProviderConfig(providerName);
|
|
151
|
-
if (providerConfig && providerConfig.apiKey) {
|
|
152
|
-
return {
|
|
153
|
-
text: providerConfig.enabled !== false ? '已启用' : '已禁用',
|
|
154
|
-
color: providerConfig.enabled !== false ? '#4caf50' : '#ff9800'
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
text: '未配置',
|
|
159
|
-
color: '#9e9e9e'
|
|
160
|
-
};
|
|
274
|
+
this.themeService.applyTheme(theme as ThemeType);
|
|
161
275
|
}
|
|
162
276
|
}
|
|
163
|
-
|
|
@@ -1,75 +1,198 @@
|
|
|
1
1
|
<div class="provider-config">
|
|
2
|
-
<h3>
|
|
2
|
+
<h3>{{ t.providers.title }}</h3>
|
|
3
3
|
|
|
4
|
-
<!--
|
|
5
|
-
<div class="
|
|
6
|
-
<div
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<button *ngIf="hasConfig(providerName)" class="btn btn-secondary"
|
|
18
|
-
(click)="testConnection(providerName)">
|
|
19
|
-
<i class="fa fa-plug"></i>
|
|
20
|
-
测试
|
|
21
|
-
</button>
|
|
22
|
-
<button *ngIf="hasConfig(providerName)" class="btn btn-danger" (click)="removeProvider(providerName)">
|
|
23
|
-
<i class="fa fa-trash"></i>
|
|
24
|
-
删除
|
|
25
|
-
</button>
|
|
26
|
-
</div>
|
|
4
|
+
<!-- 云端提供商分组 -->
|
|
5
|
+
<div class="provider-section">
|
|
6
|
+
<div class="section-header">
|
|
7
|
+
<i class="fa fa-cloud"></i>
|
|
8
|
+
<h4>{{ t.providers.cloudProviders }}</h4>
|
|
9
|
+
<span class="section-desc">{{ t.providers.cloudProvidersDesc }}</span>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="providers-grid">
|
|
13
|
+
<div *ngFor="let providerName of Object.keys(cloudProviderTemplates)"
|
|
14
|
+
class="provider-card"
|
|
15
|
+
[class.configured]="hasConfig(providerName)"
|
|
16
|
+
[class.expanded]="expandedProvider === providerName">
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<label>显示名称</label>
|
|
33
|
-
<input type="text" class="form-control" [(ngModel)]="configs[providerName].displayName">
|
|
18
|
+
<!-- 卡片头部 -->
|
|
19
|
+
<div class="card-header" (click)="toggleExpand(providerName)">
|
|
20
|
+
<div class="provider-icon" [class.local]="isLocalProvider(providerName)">
|
|
21
|
+
<i class="fa" [ngClass]="getProviderIcon(providerName)"></i>
|
|
34
22
|
</div>
|
|
35
|
-
<div class="
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
23
|
+
<div class="provider-info">
|
|
24
|
+
<h5>{{ t.providerNames[providerName] || cloudProviderTemplates[providerName].name }}</h5>
|
|
25
|
+
<span class="status-badge" [class.active]="configs[providerName]?.enabled">
|
|
26
|
+
{{ hasConfig(providerName) ? (configs[providerName]?.enabled ? t.common.enabled : t.common.disabled) : t.common.notConfigured }}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="expand-icon">
|
|
30
|
+
<i class="fa" [ngClass]="expandedProvider === providerName ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- 卡片内容(展开时显示) -->
|
|
35
|
+
<div class="card-body" *ngIf="expandedProvider === providerName">
|
|
36
|
+
<div class="config-form">
|
|
37
|
+
<div class="form-row">
|
|
38
|
+
<div class="form-group">
|
|
39
|
+
<label>{{ t.providers.displayName }}</label>
|
|
40
|
+
<input type="text" class="form-control" [(ngModel)]="configs[providerName].displayName" *ngIf="configs[providerName]">
|
|
41
|
+
</div>
|
|
42
|
+
<div class="form-group">
|
|
43
|
+
<label>{{ t.providers.status }}</label>
|
|
44
|
+
<div class="toggle-switch">
|
|
45
|
+
<input type="checkbox" [id]="'enabled-' + providerName"
|
|
46
|
+
[checked]="configs[providerName]?.enabled"
|
|
47
|
+
(change)="toggleProviderEnabled(providerName)">
|
|
48
|
+
<label [for]="'enabled-' + providerName">
|
|
49
|
+
{{ configs[providerName]?.enabled ? t.common.enabled : t.common.disabled }}
|
|
50
|
+
</label>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div *ngFor="let field of cloudProviderTemplates[providerName].fields" class="form-group">
|
|
56
|
+
<label>
|
|
57
|
+
{{ t.providers[field.key] || field.label }}
|
|
58
|
+
<span *ngIf="isRequired(field)" class="required">*</span>
|
|
43
59
|
</label>
|
|
60
|
+
<div class="input-with-toggle" *ngIf="configs[providerName] && (getFieldType(field) === 'text' || getFieldType(field) === 'password')">
|
|
61
|
+
<input
|
|
62
|
+
[type]="isPasswordField(field) && !isPasswordVisible(providerName, field.key) ? 'password' : 'text'"
|
|
63
|
+
class="form-control"
|
|
64
|
+
[class]="getInputValidationClass(providerName, field.key)"
|
|
65
|
+
[placeholder]="field.placeholder || ''"
|
|
66
|
+
[(ngModel)]="configs[providerName][field.key]">
|
|
67
|
+
<button *ngIf="isPasswordField(field)"
|
|
68
|
+
type="button"
|
|
69
|
+
class="btn-toggle-visibility"
|
|
70
|
+
(click)="togglePasswordVisibility(providerName, field.key)">
|
|
71
|
+
<i class="fa" [ngClass]="isPasswordVisible(providerName, field.key) ? 'fa-eye-slash' : 'fa-eye'"></i>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
<!-- API Key 验证提示 -->
|
|
75
|
+
<div *ngIf="field.key === 'apiKey' && configs[providerName]?.[field.key]"
|
|
76
|
+
class="validation-feedback"
|
|
77
|
+
[class.valid]="validateApiKeyFormat(providerName, configs[providerName][field.key]).valid"
|
|
78
|
+
[class.invalid]="!validateApiKeyFormat(providerName, configs[providerName][field.key]).valid">
|
|
79
|
+
<i class="fa" [ngClass]="validateApiKeyFormat(providerName, configs[providerName][field.key]).valid ? 'fa-check-circle' : 'fa-exclamation-circle'"></i>
|
|
80
|
+
{{ validateApiKeyFormat(providerName, configs[providerName][field.key]).message || '格式正确' }}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="form-actions">
|
|
85
|
+
<button class="btn btn-primary" (click)="saveConfig(providerName)">
|
|
86
|
+
<i class="fa fa-save"></i> {{ t.providers.saveConfig }}
|
|
87
|
+
</button>
|
|
88
|
+
<button class="btn btn-secondary" (click)="testConnection(providerName)">
|
|
89
|
+
<i class="fa fa-plug"></i> {{ t.providers.testConnection }}
|
|
90
|
+
</button>
|
|
44
91
|
</div>
|
|
45
92
|
</div>
|
|
46
93
|
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
47
97
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
<!-- 本地提供商分组 -->
|
|
99
|
+
<div class="provider-section local">
|
|
100
|
+
<div class="section-header">
|
|
101
|
+
<i class="fa fa-server"></i>
|
|
102
|
+
<h4>{{ t.providers.localProviders }}</h4>
|
|
103
|
+
<span class="section-desc">{{ t.providers.localProvidersDesc }}</span>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="providers-grid">
|
|
107
|
+
<div *ngFor="let providerName of Object.keys(localProviderTemplates)"
|
|
108
|
+
class="provider-card"
|
|
109
|
+
[class.configured]="hasConfig(providerName)"
|
|
110
|
+
[class.expanded]="expandedProvider === providerName">
|
|
111
|
+
|
|
112
|
+
<!-- 卡片头部 -->
|
|
113
|
+
<div class="card-header" (click)="toggleExpand(providerName)">
|
|
114
|
+
<div class="provider-icon local">
|
|
115
|
+
<i class="fa" [ngClass]="getProviderIcon(providerName)"></i>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="provider-info">
|
|
118
|
+
<h5>{{ t.providerNames[providerName] || localProviderTemplates[providerName].name }}</h5>
|
|
119
|
+
<div class="status-row">
|
|
120
|
+
<span class="status-badge" [class.active]="configs[providerName]?.enabled">
|
|
121
|
+
{{ hasConfig(providerName) ? (configs[providerName]?.enabled ? t.common.enabled : t.common.disabled) : t.common.notConfigured }}
|
|
122
|
+
</span>
|
|
123
|
+
<!-- 在线状态指示器 -->
|
|
124
|
+
<span class="online-status" [style.color]="getLocalStatus(providerName).color">
|
|
125
|
+
<i class="fa" [ngClass]="getLocalStatus(providerName).icon"></i>
|
|
126
|
+
{{ getLocalStatus(providerName).text }}
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="expand-icon">
|
|
131
|
+
<i class="fa" [ngClass]="expandedProvider === providerName ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
|
|
132
|
+
</div>
|
|
64
133
|
</div>
|
|
65
134
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
135
|
+
<!-- 卡片内容(展开时显示) -->
|
|
136
|
+
<div class="card-body" *ngIf="expandedProvider === providerName">
|
|
137
|
+
<div class="config-form">
|
|
138
|
+
<div class="form-row">
|
|
139
|
+
<div class="form-group">
|
|
140
|
+
<label>{{ t.providers.displayName }}</label>
|
|
141
|
+
<input type="text" class="form-control" [(ngModel)]="configs[providerName].displayName" *ngIf="configs[providerName]">
|
|
142
|
+
</div>
|
|
143
|
+
<div class="form-group">
|
|
144
|
+
<label>{{ t.providers.status }}</label>
|
|
145
|
+
<div class="toggle-switch">
|
|
146
|
+
<input type="checkbox" [id]="'enabled-' + providerName"
|
|
147
|
+
[checked]="configs[providerName]?.enabled"
|
|
148
|
+
(change)="toggleProviderEnabled(providerName)">
|
|
149
|
+
<label [for]="'enabled-' + providerName">
|
|
150
|
+
{{ configs[providerName]?.enabled ? t.common.enabled : t.common.disabled }}
|
|
151
|
+
</label>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div *ngFor="let field of localProviderTemplates[providerName].fields" class="form-group">
|
|
157
|
+
<label>
|
|
158
|
+
{{ t.providers[field.key] || field.label }}
|
|
159
|
+
<span *ngIf="isRequired(field)" class="required">*</span>
|
|
160
|
+
</label>
|
|
161
|
+
<div class="input-with-toggle" *ngIf="configs[providerName] && (getFieldType(field) === 'text' || getFieldType(field) === 'password')">
|
|
162
|
+
<input
|
|
163
|
+
[type]="isPasswordField(field) && !isPasswordVisible(providerName, field.key) ? 'password' : 'text'"
|
|
164
|
+
class="form-control"
|
|
165
|
+
[class]="getInputValidationClass(providerName, field.key)"
|
|
166
|
+
[placeholder]="field.placeholder || ''"
|
|
167
|
+
[(ngModel)]="configs[providerName][field.key]">
|
|
168
|
+
<button *ngIf="isPasswordField(field)"
|
|
169
|
+
type="button"
|
|
170
|
+
class="btn-toggle-visibility"
|
|
171
|
+
(click)="togglePasswordVisibility(providerName, field.key)">
|
|
172
|
+
<i class="fa" [ngClass]="isPasswordVisible(providerName, field.key) ? 'fa-eye-slash' : 'fa-eye'"></i>
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
<!-- API Key 验证提示 -->
|
|
176
|
+
<div *ngIf="field.key === 'apiKey' && configs[providerName]?.[field.key]"
|
|
177
|
+
class="validation-feedback"
|
|
178
|
+
[class.valid]="validateApiKeyFormat(providerName, configs[providerName][field.key]).valid"
|
|
179
|
+
[class.invalid]="!validateApiKeyFormat(providerName, configs[providerName][field.key]).valid">
|
|
180
|
+
<i class="fa" [ngClass]="validateApiKeyFormat(providerName, configs[providerName][field.key]).valid ? 'fa-check-circle' : 'fa-exclamation-circle'"></i>
|
|
181
|
+
{{ validateApiKeyFormat(providerName, configs[providerName][field.key]).message || '格式正确' }}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div class="form-actions">
|
|
186
|
+
<button class="btn btn-primary" (click)="saveConfig(providerName)">
|
|
187
|
+
<i class="fa fa-save"></i> {{ t.providers.saveConfig }}
|
|
188
|
+
</button>
|
|
189
|
+
<button class="btn btn-info" (click)="testConnection(providerName)">
|
|
190
|
+
<i class="fa fa-wifi"></i> {{ t.providers.detectService }}
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
71
194
|
</div>
|
|
72
195
|
</div>
|
|
73
196
|
</div>
|
|
74
197
|
</div>
|
|
75
|
-
</div>
|
|
198
|
+
</div>
|