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
|
@@ -11,6 +11,7 @@ export interface AiSidebarConfig {
|
|
|
11
11
|
showInToolbar?: boolean;
|
|
12
12
|
sidebarVisible?: boolean;
|
|
13
13
|
sidebarCollapsed?: boolean;
|
|
14
|
+
sidebarWidth?: number;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -24,8 +25,15 @@ export class AiSidebarService {
|
|
|
24
25
|
private sidebarComponentRef: ComponentRef<AiSidebarComponent> | null = null;
|
|
25
26
|
private sidebarElement: HTMLElement | null = null;
|
|
26
27
|
private styleElement: HTMLStyleElement | null = null;
|
|
28
|
+
private resizeHandle: HTMLElement | null = null;
|
|
27
29
|
private _isVisible = false;
|
|
28
|
-
|
|
30
|
+
|
|
31
|
+
// Resize constants
|
|
32
|
+
private readonly MIN_WIDTH = 280;
|
|
33
|
+
private readonly MAX_WIDTH = 500;
|
|
34
|
+
private readonly DEFAULT_WIDTH = 320;
|
|
35
|
+
private currentWidth: number = this.DEFAULT_WIDTH;
|
|
36
|
+
private isResizing = false;
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
39
|
* 侧边栏是否可见
|
|
@@ -122,24 +130,76 @@ export class AiSidebarService {
|
|
|
122
130
|
|
|
123
131
|
// 获取 DOM 元素
|
|
124
132
|
const domElem = (this.sidebarComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
|
|
133
|
+
// 直接设置组件 host 元素的样式 - 确保 flex 布局正确
|
|
134
|
+
domElem.style.display = 'flex';
|
|
135
|
+
domElem.style.flexDirection = 'column';
|
|
136
|
+
domElem.style.height = '100%';
|
|
137
|
+
domElem.style.width = '100%';
|
|
138
|
+
domElem.style.overflow = 'hidden';
|
|
125
139
|
|
|
126
140
|
// 创建 wrapper 元素 - 使用固定定位
|
|
127
141
|
const wrapper = document.createElement('div');
|
|
128
142
|
wrapper.className = 'ai-sidebar-wrapper';
|
|
143
|
+
|
|
144
|
+
// 加载保存的宽度或使用默认值
|
|
145
|
+
this.currentWidth = this.loadSidebarWidth();
|
|
146
|
+
|
|
147
|
+
// 获取视口高度 - 使用绝对像素值确保滚动容器正确计算
|
|
148
|
+
const viewportHeight = window.innerHeight;
|
|
129
149
|
wrapper.style.cssText = `
|
|
130
150
|
position: fixed;
|
|
131
151
|
left: 0;
|
|
132
152
|
top: 0;
|
|
133
|
-
width: ${this.
|
|
134
|
-
height:
|
|
153
|
+
width: ${this.currentWidth}px;
|
|
154
|
+
height: ${viewportHeight}px;
|
|
135
155
|
display: flex;
|
|
136
156
|
flex-direction: column;
|
|
137
157
|
background: var(--bs-body-bg, #1e1e1e);
|
|
138
158
|
border-right: 1px solid var(--bs-border-color, #333);
|
|
139
159
|
box-shadow: 2px 0 10px rgba(0,0,0,0.3);
|
|
140
160
|
z-index: 1000;
|
|
161
|
+
overflow: hidden;
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
// 监听窗口大小变化,动态更新高度
|
|
165
|
+
const resizeHandler = () => {
|
|
166
|
+
wrapper.style.height = `${window.innerHeight}px`;
|
|
167
|
+
};
|
|
168
|
+
window.addEventListener('resize', resizeHandler);
|
|
169
|
+
// 存储 handler 以便在销毁时移除
|
|
170
|
+
(wrapper as any)._resizeHandler = resizeHandler;
|
|
171
|
+
|
|
172
|
+
// 创建 resize handle(拖动条)
|
|
173
|
+
const resizeHandle = document.createElement('div');
|
|
174
|
+
resizeHandle.className = 'ai-sidebar-resize-handle';
|
|
175
|
+
resizeHandle.style.cssText = `
|
|
176
|
+
position: absolute;
|
|
177
|
+
top: 0;
|
|
178
|
+
right: -4px;
|
|
179
|
+
width: 8px;
|
|
180
|
+
height: 100%;
|
|
181
|
+
cursor: ew-resize;
|
|
182
|
+
background: transparent;
|
|
183
|
+
z-index: 1001;
|
|
184
|
+
transition: background 0.2s;
|
|
141
185
|
`;
|
|
142
186
|
|
|
187
|
+
// 鼠标悬停时显示高亮
|
|
188
|
+
resizeHandle.addEventListener('mouseenter', () => {
|
|
189
|
+
resizeHandle.style.background = 'var(--ai-primary, #4dabf7)';
|
|
190
|
+
});
|
|
191
|
+
resizeHandle.addEventListener('mouseleave', () => {
|
|
192
|
+
if (!this.isResizing) {
|
|
193
|
+
resizeHandle.style.background = 'transparent';
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 添加拖动逻辑
|
|
198
|
+
this.setupResizeHandler(resizeHandle, wrapper, viewportHeight);
|
|
199
|
+
|
|
200
|
+
wrapper.appendChild(resizeHandle);
|
|
201
|
+
this.resizeHandle = resizeHandle;
|
|
202
|
+
|
|
143
203
|
wrapper.appendChild(domElem);
|
|
144
204
|
|
|
145
205
|
// 插入到 body
|
|
@@ -164,6 +224,14 @@ export class AiSidebarService {
|
|
|
164
224
|
// 移除注入的 CSS
|
|
165
225
|
this.removeLayoutCSS();
|
|
166
226
|
|
|
227
|
+
// 移除 resize 监听器
|
|
228
|
+
if (this.sidebarElement) {
|
|
229
|
+
const handler = (this.sidebarElement as any)._resizeHandler;
|
|
230
|
+
if (handler) {
|
|
231
|
+
window.removeEventListener('resize', handler);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
167
235
|
if (this.sidebarComponentRef) {
|
|
168
236
|
this.appRef.detachView(this.sidebarComponentRef.hostView);
|
|
169
237
|
this.sidebarComponentRef.destroy();
|
|
@@ -211,7 +279,7 @@ export class AiSidebarService {
|
|
|
211
279
|
|
|
212
280
|
/**
|
|
213
281
|
* 注入布局 CSS - 使用 margin-left 把主内容推开
|
|
214
|
-
*
|
|
282
|
+
*
|
|
215
283
|
* 固定定位方案:侧边栏 fixed,主内容区 margin-left
|
|
216
284
|
*/
|
|
217
285
|
private injectLayoutCSS(): void {
|
|
@@ -220,7 +288,7 @@ export class AiSidebarService {
|
|
|
220
288
|
style.textContent = `
|
|
221
289
|
/* 用 margin-left 把 app-root 推开,为侧边栏腾出空间 */
|
|
222
290
|
app-root {
|
|
223
|
-
margin-left: ${this.
|
|
291
|
+
margin-left: ${this.currentWidth}px !important;
|
|
224
292
|
}
|
|
225
293
|
`;
|
|
226
294
|
|
|
@@ -238,6 +306,90 @@ export class AiSidebarService {
|
|
|
238
306
|
}
|
|
239
307
|
}
|
|
240
308
|
|
|
309
|
+
/**
|
|
310
|
+
* 设置 resize handle 拖动逻辑
|
|
311
|
+
*/
|
|
312
|
+
private setupResizeHandler(handle: HTMLElement, wrapper: HTMLElement, viewportHeight: number): void {
|
|
313
|
+
let startX: number;
|
|
314
|
+
let startWidth: number;
|
|
315
|
+
|
|
316
|
+
const onMouseDown = (e: MouseEvent) => {
|
|
317
|
+
e.preventDefault();
|
|
318
|
+
this.isResizing = true;
|
|
319
|
+
startX = e.clientX;
|
|
320
|
+
startWidth = this.currentWidth;
|
|
321
|
+
|
|
322
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
323
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
324
|
+
document.body.style.cursor = 'ew-resize';
|
|
325
|
+
document.body.style.userSelect = 'none';
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
329
|
+
if (!this.isResizing) return;
|
|
330
|
+
|
|
331
|
+
const delta = e.clientX - startX;
|
|
332
|
+
let newWidth = startWidth + delta;
|
|
333
|
+
|
|
334
|
+
// 限制宽度范围
|
|
335
|
+
newWidth = Math.max(this.MIN_WIDTH, Math.min(this.MAX_WIDTH, newWidth));
|
|
336
|
+
|
|
337
|
+
this.currentWidth = newWidth;
|
|
338
|
+
wrapper.style.width = `${newWidth}px`;
|
|
339
|
+
|
|
340
|
+
// 更新 app-root 的 margin-left
|
|
341
|
+
this.updateLayoutCSS(newWidth);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const onMouseUp = () => {
|
|
345
|
+
this.isResizing = false;
|
|
346
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
347
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
348
|
+
document.body.style.cursor = '';
|
|
349
|
+
document.body.style.userSelect = '';
|
|
350
|
+
handle.style.background = 'transparent';
|
|
351
|
+
|
|
352
|
+
// 保存宽度到配置
|
|
353
|
+
this.saveSidebarWidth(this.currentWidth);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
handle.addEventListener('mousedown', onMouseDown);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 更新布局 CSS(resize 时调用)
|
|
361
|
+
*/
|
|
362
|
+
private updateLayoutCSS(width: number): void {
|
|
363
|
+
if (this.styleElement) {
|
|
364
|
+
this.styleElement.textContent = `
|
|
365
|
+
app-root {
|
|
366
|
+
margin-left: ${width}px !important;
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 加载保存的侧边栏宽度
|
|
374
|
+
*/
|
|
375
|
+
private loadSidebarWidth(): number {
|
|
376
|
+
const pluginConfig = this.getPluginConfig();
|
|
377
|
+
const savedWidth = pluginConfig.sidebarWidth;
|
|
378
|
+
if (savedWidth && savedWidth >= this.MIN_WIDTH && savedWidth <= this.MAX_WIDTH) {
|
|
379
|
+
return savedWidth;
|
|
380
|
+
}
|
|
381
|
+
return this.DEFAULT_WIDTH;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 保存侧边栏宽度到配置
|
|
386
|
+
*/
|
|
387
|
+
private saveSidebarWidth(width: number): void {
|
|
388
|
+
const pluginConfig = this.getPluginConfig();
|
|
389
|
+
pluginConfig.sidebarWidth = width;
|
|
390
|
+
this.savePluginConfig(pluginConfig);
|
|
391
|
+
}
|
|
392
|
+
|
|
241
393
|
/**
|
|
242
394
|
* 获取插件配置
|
|
243
395
|
*/
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主题服务 - 统一管理所有 AI 助手主题
|
|
3
|
+
* 通过动态 <style> 注入实现主题切换
|
|
4
|
+
*/
|
|
5
|
+
import { Injectable, OnDestroy } from '@angular/core';
|
|
6
|
+
import { Subject, Subscription } from 'rxjs';
|
|
7
|
+
import { debounceTime } from 'rxjs/operators';
|
|
8
|
+
import { ConfigService } from 'tabby-core';
|
|
9
|
+
import { ConfigProviderService } from './config-provider.service';
|
|
10
|
+
|
|
11
|
+
export type ThemeType = 'auto' | 'light' | 'dark' | 'pixel' | 'tech';
|
|
12
|
+
|
|
13
|
+
// 主题变量定义
|
|
14
|
+
const THEME_VARIABLES: Record<Exclude<ThemeType, 'auto'>, Record<string, string>> = {
|
|
15
|
+
light: {
|
|
16
|
+
// 主色调
|
|
17
|
+
'ai-primary': '#007bff',
|
|
18
|
+
'ai-primary-hover': '#0056b3',
|
|
19
|
+
'ai-secondary': '#6c757d',
|
|
20
|
+
'ai-success': '#28a745',
|
|
21
|
+
'ai-warning': '#ffc107',
|
|
22
|
+
'ai-danger': '#dc3545',
|
|
23
|
+
'ai-info': '#17a2b8',
|
|
24
|
+
// 风险级别颜色
|
|
25
|
+
'ai-risk-low': '#28a745',
|
|
26
|
+
'ai-risk-medium': '#ffc107',
|
|
27
|
+
'ai-risk-high': '#fd7e14',
|
|
28
|
+
'ai-risk-critical': '#dc3545',
|
|
29
|
+
// 聊天消息颜色
|
|
30
|
+
'ai-user-message': '#e3f2fd',
|
|
31
|
+
'ai-assistant-message': '#f5f5f5',
|
|
32
|
+
'ai-system-message': '#fff3cd',
|
|
33
|
+
// 背景和边框
|
|
34
|
+
'ai-bg-primary': '#ffffff',
|
|
35
|
+
'ai-bg-secondary': '#f8f9fa',
|
|
36
|
+
'ai-bg-tertiary': '#e9ecef',
|
|
37
|
+
'ai-text-primary': '#212529',
|
|
38
|
+
'ai-text-secondary': '#6c757d',
|
|
39
|
+
'ai-border': '#dee2e6',
|
|
40
|
+
'ai-border-radius': '0.375rem',
|
|
41
|
+
'ai-box-shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
|
42
|
+
// 字体
|
|
43
|
+
'ai-font-family': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
44
|
+
'ai-font-size-base': '14px',
|
|
45
|
+
// 其他
|
|
46
|
+
'ai-dark': '#212529',
|
|
47
|
+
'ai-light': '#f8f9fa',
|
|
48
|
+
'ai-transition-duration': '0.3s'
|
|
49
|
+
},
|
|
50
|
+
dark: {
|
|
51
|
+
// 主色调
|
|
52
|
+
'ai-primary': '#4dabf7',
|
|
53
|
+
'ai-primary-hover': '#339af0',
|
|
54
|
+
'ai-secondary': '#adb5bd',
|
|
55
|
+
'ai-success': '#51cf66',
|
|
56
|
+
'ai-warning': '#fcc419',
|
|
57
|
+
'ai-danger': '#ff6b6b',
|
|
58
|
+
'ai-info': '#22b8cf',
|
|
59
|
+
// 风险级别颜色
|
|
60
|
+
'ai-risk-low': '#51cf66',
|
|
61
|
+
'ai-risk-medium': '#fcc419',
|
|
62
|
+
'ai-risk-high': '#ff922b',
|
|
63
|
+
'ai-risk-critical': '#ff6b6b',
|
|
64
|
+
// 聊天消息颜色
|
|
65
|
+
'ai-user-message': '#1e3a5f',
|
|
66
|
+
'ai-assistant-message': '#2d3748',
|
|
67
|
+
'ai-system-message': '#3a3a3a',
|
|
68
|
+
// 背景和边框
|
|
69
|
+
'ai-bg-primary': '#1a1a1a',
|
|
70
|
+
'ai-bg-secondary': '#2d2d2d',
|
|
71
|
+
'ai-bg-tertiary': '#3d3d3d',
|
|
72
|
+
'ai-text-primary': '#f8f9fa',
|
|
73
|
+
'ai-text-secondary': '#adb5bd',
|
|
74
|
+
'ai-border': '#4a4a4a',
|
|
75
|
+
'ai-border-radius': '0.375rem',
|
|
76
|
+
'ai-box-shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.3)',
|
|
77
|
+
// 字体
|
|
78
|
+
'ai-font-family': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
79
|
+
'ai-font-size-base': '14px',
|
|
80
|
+
// 其他
|
|
81
|
+
'ai-dark': '#212529',
|
|
82
|
+
'ai-light': '#f8f9fa',
|
|
83
|
+
'ai-transition-duration': '0.3s'
|
|
84
|
+
},
|
|
85
|
+
pixel: {
|
|
86
|
+
// 主色调 - 经典 GameBoy 绿
|
|
87
|
+
'ai-primary': '#9bbc0f',
|
|
88
|
+
'ai-primary-hover': '#8bac0f',
|
|
89
|
+
'ai-secondary': '#306230',
|
|
90
|
+
'ai-success': '#9bbc0f',
|
|
91
|
+
'ai-warning': '#ffeb3b',
|
|
92
|
+
'ai-danger': '#f44336',
|
|
93
|
+
'ai-info': '#03a9f4',
|
|
94
|
+
// 风险级别颜色
|
|
95
|
+
'ai-risk-low': '#9bbc0f',
|
|
96
|
+
'ai-risk-medium': '#ffeb3b',
|
|
97
|
+
'ai-risk-high': '#ff9800',
|
|
98
|
+
'ai-risk-critical': '#f44336',
|
|
99
|
+
// 聊天消息颜色
|
|
100
|
+
'ai-user-message': '#0f380f',
|
|
101
|
+
'ai-assistant-message': '#306230',
|
|
102
|
+
'ai-system-message': '#1a1a1a',
|
|
103
|
+
// 背景和边框
|
|
104
|
+
'ai-bg-primary': '#0f380f',
|
|
105
|
+
'ai-bg-secondary': '#1a2a1a',
|
|
106
|
+
'ai-bg-tertiary': '#306230',
|
|
107
|
+
'ai-text-primary': '#9bbc0f',
|
|
108
|
+
'ai-text-secondary': '#8bac0f',
|
|
109
|
+
'ai-border': '#9bbc0f',
|
|
110
|
+
'ai-border-radius': '0',
|
|
111
|
+
'ai-box-shadow': '4px 4px 0 rgba(15, 56, 15, 0.8)',
|
|
112
|
+
// 字体 - 像素风格
|
|
113
|
+
'ai-font-family': "'Courier New', 'Press Start 2P', monospace",
|
|
114
|
+
'ai-font-size-base': '12px',
|
|
115
|
+
// 其他
|
|
116
|
+
'ai-dark': '#0f380f',
|
|
117
|
+
'ai-light': '#306230',
|
|
118
|
+
'ai-transition-duration': '0s'
|
|
119
|
+
},
|
|
120
|
+
tech: {
|
|
121
|
+
// 主色调 - 霓虹赛博朋克
|
|
122
|
+
'ai-primary': '#00fff9',
|
|
123
|
+
'ai-primary-hover': '#00e6e0',
|
|
124
|
+
'ai-secondary': '#adb5bd',
|
|
125
|
+
'ai-success': '#00ff88',
|
|
126
|
+
'ai-warning': '#ff00ff',
|
|
127
|
+
'ai-danger': '#ff3366',
|
|
128
|
+
'ai-info': '#00bfff',
|
|
129
|
+
// 风险级别颜色
|
|
130
|
+
'ai-risk-low': '#00ff88',
|
|
131
|
+
'ai-risk-medium': '#ff00ff',
|
|
132
|
+
'ai-risk-high': '#ff6600',
|
|
133
|
+
'ai-risk-critical': '#ff3366',
|
|
134
|
+
// 聊天消息颜色
|
|
135
|
+
'ai-user-message': 'rgba(0, 255, 249, 0.1)',
|
|
136
|
+
'ai-assistant-message': 'rgba(255, 0, 255, 0.1)',
|
|
137
|
+
'ai-system-message': 'rgba(0, 255, 136, 0.1)',
|
|
138
|
+
// 背景和边框
|
|
139
|
+
'ai-bg-primary': '#0a0a0f',
|
|
140
|
+
'ai-bg-secondary': '#12121a',
|
|
141
|
+
'ai-bg-tertiary': '#1a1a2e',
|
|
142
|
+
'ai-text-primary': '#00fff9',
|
|
143
|
+
'ai-text-secondary': 'rgba(0, 255, 249, 0.7)',
|
|
144
|
+
'ai-border': 'rgba(0, 255, 249, 0.3)',
|
|
145
|
+
'ai-border-radius': '4px',
|
|
146
|
+
'ai-box-shadow': '0 0 20px rgba(0, 255, 249, 0.2)',
|
|
147
|
+
// 字体 - 科幻感
|
|
148
|
+
'ai-font-family': "'Segoe UI', 'Share Tech Mono', monospace",
|
|
149
|
+
'ai-font-size-base': '14px',
|
|
150
|
+
// 其他
|
|
151
|
+
'ai-dark': '#0a0a0f',
|
|
152
|
+
'ai-light': '#12121a',
|
|
153
|
+
'ai-transition-duration': '0.3s'
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
@Injectable({
|
|
158
|
+
providedIn: 'root'
|
|
159
|
+
})
|
|
160
|
+
export class ThemeService implements OnDestroy {
|
|
161
|
+
private currentTheme$ = new Subject<ThemeType>();
|
|
162
|
+
private tabbySubscription?: Subscription;
|
|
163
|
+
private styleElement: HTMLStyleElement;
|
|
164
|
+
readonly theme$ = this.currentTheme$.asObservable();
|
|
165
|
+
|
|
166
|
+
private readonly allThemeClasses = [
|
|
167
|
+
'ai-theme-auto', 'ai-theme-light', 'ai-theme-dark',
|
|
168
|
+
'ai-theme-pixel', 'ai-theme-tech'
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// AI 助手容器选择器
|
|
172
|
+
private readonly containerSelectors = [
|
|
173
|
+
'.ai-chat-interface',
|
|
174
|
+
'.ai-settings-tab',
|
|
175
|
+
'.ai-assistant',
|
|
176
|
+
'.ai-sidebar-container',
|
|
177
|
+
'.ai-chat-modal-left'
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
constructor(
|
|
181
|
+
private config: ConfigProviderService,
|
|
182
|
+
private tabbyConfig: ConfigService
|
|
183
|
+
) {
|
|
184
|
+
// 创建并注入动态样式元素
|
|
185
|
+
this.styleElement = document.createElement('style');
|
|
186
|
+
this.styleElement.id = 'ai-assistant-dynamic-theme';
|
|
187
|
+
document.head.appendChild(this.styleElement);
|
|
188
|
+
|
|
189
|
+
this.init();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private init(): void {
|
|
193
|
+
const savedTheme = this.config.get<string>('theme', 'auto') as ThemeType;
|
|
194
|
+
this.setTheme(savedTheme);
|
|
195
|
+
|
|
196
|
+
// 监听 Tabby 主题变化(带防抖)
|
|
197
|
+
this.tabbySubscription = this.tabbyConfig.changed$.pipe(
|
|
198
|
+
debounceTime(100)
|
|
199
|
+
).subscribe(() => {
|
|
200
|
+
const currentTheme = this.config.get<string>('theme', 'auto');
|
|
201
|
+
if (currentTheme === 'auto') {
|
|
202
|
+
this.applyTheme('auto');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 获取当前主题
|
|
209
|
+
*/
|
|
210
|
+
getCurrentTheme(): ThemeType {
|
|
211
|
+
return this.config.get<string>('theme', 'auto') as ThemeType;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 设置并应用主题
|
|
216
|
+
*/
|
|
217
|
+
setTheme(theme: ThemeType): void {
|
|
218
|
+
this.config.set('theme', theme);
|
|
219
|
+
this.applyTheme(theme);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 核心方法:动态注入主题样式
|
|
224
|
+
*/
|
|
225
|
+
applyTheme(theme: ThemeType): void {
|
|
226
|
+
this.currentTheme$.next(theme);
|
|
227
|
+
|
|
228
|
+
// 确定实际生效的主题
|
|
229
|
+
let effectiveTheme: ThemeType = theme;
|
|
230
|
+
if (theme === 'auto') {
|
|
231
|
+
effectiveTheme = this.getTabbyEffectiveTheme();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 1. 生成 CSS 变量样式
|
|
235
|
+
const cssVariables = this.buildCssVariables(effectiveTheme);
|
|
236
|
+
// 2. 生成主题特定样式
|
|
237
|
+
const themeStyles = this.buildThemeStyles(effectiveTheme);
|
|
238
|
+
// 3. 注入完整样式到 DOM
|
|
239
|
+
this.styleElement.innerHTML = `
|
|
240
|
+
/* AI Assistant Dynamic Theme - ${theme} (effective: ${effectiveTheme}) */
|
|
241
|
+
:root,
|
|
242
|
+
html,
|
|
243
|
+
body,
|
|
244
|
+
${this.containerSelectors.join(',\n')} {
|
|
245
|
+
${cssVariables}
|
|
246
|
+
}
|
|
247
|
+
${themeStyles}
|
|
248
|
+
`.trim();
|
|
249
|
+
|
|
250
|
+
// 4. 更新类名和 data 属性
|
|
251
|
+
this.updateBodyClasses(theme, effectiveTheme);
|
|
252
|
+
|
|
253
|
+
// 5. 触发自定义事件
|
|
254
|
+
window.dispatchEvent(new CustomEvent('ai-theme-changed', {
|
|
255
|
+
detail: { theme, effectiveTheme }
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
console.log('[ThemeService] Theme applied dynamically:', { theme, effectiveTheme });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 生成 CSS 变量字符串
|
|
263
|
+
*/
|
|
264
|
+
private buildCssVariables(theme: ThemeType): string {
|
|
265
|
+
const vars = THEME_VARIABLES[theme] || THEME_VARIABLES.dark;
|
|
266
|
+
return Object.entries(vars)
|
|
267
|
+
.map(([key, value]) => ` --${key}: ${value} !important;`)
|
|
268
|
+
.join('\n');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 生成主题特定样式(像素风格、科技风格等)
|
|
273
|
+
*/
|
|
274
|
+
private buildThemeStyles(theme: ThemeType): string {
|
|
275
|
+
if (theme === 'pixel') {
|
|
276
|
+
return `
|
|
277
|
+
/* Pixel theme specific styles */
|
|
278
|
+
${this.containerSelectors.join(',\n')} {
|
|
279
|
+
/* 按钮像素化 */
|
|
280
|
+
.btn {
|
|
281
|
+
border: 3px solid var(--ai-border) !important;
|
|
282
|
+
border-radius: 0 !important;
|
|
283
|
+
box-shadow: 4px 4px 0 var(--ai-bg-tertiary) !important;
|
|
284
|
+
font-family: var(--ai-font-family) !important;
|
|
285
|
+
transition: none !important;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.btn:active {
|
|
289
|
+
box-shadow: none !important;
|
|
290
|
+
transform: translate(4px, 4px) !important;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* 输入框 */
|
|
294
|
+
input,
|
|
295
|
+
textarea,
|
|
296
|
+
.form-control {
|
|
297
|
+
border: 3px solid var(--ai-border) !important;
|
|
298
|
+
border-radius: 0 !important;
|
|
299
|
+
background: var(--ai-bg-primary) !important;
|
|
300
|
+
color: var(--ai-text-primary) !important;
|
|
301
|
+
font-family: var(--ai-font-family) !important;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
input:focus,
|
|
305
|
+
textarea:focus,
|
|
306
|
+
.form-control:focus {
|
|
307
|
+
outline: none !important;
|
|
308
|
+
border-color: var(--ai-primary) !important;
|
|
309
|
+
box-shadow: 4px 4px 0 var(--ai-bg-tertiary) !important;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* 聊天气泡 */
|
|
313
|
+
.message-bubble {
|
|
314
|
+
border: 3px solid var(--ai-border) !important;
|
|
315
|
+
border-radius: 0 !important;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* 标题 */
|
|
319
|
+
h2, h3, h4 {
|
|
320
|
+
font-family: var(--ai-font-family) !important;
|
|
321
|
+
letter-spacing: 1px !important;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
`.trim();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (theme === 'tech') {
|
|
328
|
+
return `
|
|
329
|
+
/* Tech theme specific styles */
|
|
330
|
+
${this.containerSelectors.join(',\n')} {
|
|
331
|
+
/* 扫描线背景 */
|
|
332
|
+
background: repeating-linear-gradient(
|
|
333
|
+
0deg,
|
|
334
|
+
transparent,
|
|
335
|
+
transparent 2px,
|
|
336
|
+
rgba(0, 255, 249, 0.03) 2px,
|
|
337
|
+
rgba(0, 255, 249, 0.03) 4px
|
|
338
|
+
) !important;
|
|
339
|
+
|
|
340
|
+
/* 发光按钮 */
|
|
341
|
+
.btn {
|
|
342
|
+
background: linear-gradient(135deg, var(--ai-bg-secondary) 0%, var(--ai-bg-tertiary) 100%) !important;
|
|
343
|
+
border: 1px solid var(--ai-primary) !important;
|
|
344
|
+
color: var(--ai-text-primary) !important;
|
|
345
|
+
box-shadow:
|
|
346
|
+
0 0 10px rgba(0, 255, 249, 0.2),
|
|
347
|
+
inset 0 0 10px rgba(0, 255, 249, 0.05) !important;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.btn:hover {
|
|
351
|
+
box-shadow:
|
|
352
|
+
0 0 20px rgba(0, 255, 249, 0.4),
|
|
353
|
+
inset 0 0 20px rgba(0, 255, 249, 0.1) !important;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* 发光输入框 */
|
|
357
|
+
input,
|
|
358
|
+
textarea,
|
|
359
|
+
.form-control {
|
|
360
|
+
border: 1px solid var(--ai-primary) !important;
|
|
361
|
+
background: var(--ai-bg-primary) !important;
|
|
362
|
+
color: var(--ai-text-primary) !important;
|
|
363
|
+
box-shadow: 0 0 10px rgba(0, 255, 249, 0.1) !important;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
input:focus,
|
|
367
|
+
textarea:focus,
|
|
368
|
+
.form-control:focus {
|
|
369
|
+
outline: none !important;
|
|
370
|
+
border-color: var(--ai-primary) !important;
|
|
371
|
+
box-shadow:
|
|
372
|
+
0 0 20px rgba(0, 255, 249, 0.3),
|
|
373
|
+
inset 0 0 10px rgba(0, 255, 249, 0.05) !important;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* 霓虹标题 */
|
|
377
|
+
h2, h3, h4 {
|
|
378
|
+
text-shadow:
|
|
379
|
+
0 0 10px var(--ai-primary),
|
|
380
|
+
0 0 20px var(--ai-primary) !important;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* 滚动条 */
|
|
384
|
+
::-webkit-scrollbar {
|
|
385
|
+
width: 8px !important;
|
|
386
|
+
background: var(--ai-bg-secondary) !important;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
::-webkit-scrollbar-thumb {
|
|
390
|
+
background: var(--ai-primary) !important;
|
|
391
|
+
border-radius: 0 !important;
|
|
392
|
+
box-shadow: 0 0 10px var(--ai-primary) !important;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
`.trim();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return '';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 更新 body 和 html 的类名
|
|
403
|
+
*/
|
|
404
|
+
private updateBodyClasses(theme: ThemeType, effectiveTheme: ThemeType): void {
|
|
405
|
+
const root = document.documentElement;
|
|
406
|
+
const body = document.body;
|
|
407
|
+
|
|
408
|
+
// 移除所有主题类
|
|
409
|
+
this.allThemeClasses.forEach(cls => {
|
|
410
|
+
root.classList.remove(cls);
|
|
411
|
+
body.classList.remove(cls);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// 添加新主题类
|
|
415
|
+
const themeClass = `ai-theme-${theme}`;
|
|
416
|
+
const effectiveClass = `ai-theme-${effectiveTheme}`;
|
|
417
|
+
|
|
418
|
+
root.classList.add(themeClass);
|
|
419
|
+
body.classList.add(themeClass);
|
|
420
|
+
root.setAttribute('data-ai-theme', effectiveTheme);
|
|
421
|
+
body.setAttribute('data-ai-theme', effectiveTheme);
|
|
422
|
+
|
|
423
|
+
if (theme === 'auto') {
|
|
424
|
+
root.classList.add(effectiveClass);
|
|
425
|
+
body.classList.add(effectiveClass);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 更新所有 AI 容器的类
|
|
429
|
+
const containers = document.querySelectorAll(this.containerSelectors.join(','));
|
|
430
|
+
containers.forEach(container => {
|
|
431
|
+
this.allThemeClasses.forEach(cls => container.classList.remove(cls));
|
|
432
|
+
container.classList.add(themeClass);
|
|
433
|
+
container.setAttribute('data-ai-theme', effectiveTheme);
|
|
434
|
+
|
|
435
|
+
if (theme === 'auto') {
|
|
436
|
+
container.classList.add(effectiveClass);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 获取 Tabby 当前的有效主题
|
|
443
|
+
*/
|
|
444
|
+
private getTabbyEffectiveTheme(): 'light' | 'dark' {
|
|
445
|
+
const appearance = this.tabbyConfig.store?.appearance;
|
|
446
|
+
|
|
447
|
+
if (appearance) {
|
|
448
|
+
if (appearance.colorScheme) {
|
|
449
|
+
const scheme = appearance.colorScheme.toLowerCase();
|
|
450
|
+
if (scheme === 'light') return 'light';
|
|
451
|
+
if (scheme === 'dark') return 'dark';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const theme = appearance.theme?.toLowerCase() || '';
|
|
455
|
+
const darkThemes = ['hype', 'standard', 'dark', 'dracula', 'monokai', 'one-dark'];
|
|
456
|
+
if (darkThemes.some(t => theme.includes(t))) {
|
|
457
|
+
return 'dark';
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return 'dark';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 刷新所有容器的主题类
|
|
466
|
+
*/
|
|
467
|
+
refreshContainers(): void {
|
|
468
|
+
const theme = this.config.get<string>('theme', 'auto') as ThemeType;
|
|
469
|
+
this.applyTheme(theme);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
ngOnDestroy(): void {
|
|
473
|
+
// 清理动态样式
|
|
474
|
+
if (this.styleElement && this.styleElement.parentNode) {
|
|
475
|
+
this.styleElement.parentNode.removeChild(this.styleElement);
|
|
476
|
+
}
|
|
477
|
+
this.tabbySubscription?.unsubscribe();
|
|
478
|
+
this.currentTheme$.complete();
|
|
479
|
+
}
|
|
480
|
+
}
|