@huyooo/ai-chat-frontend-react 0.1.6 → 0.2.0

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.
Files changed (91) hide show
  1. package/README.md +368 -0
  2. package/dist/index.css +2575 -0
  3. package/dist/index.css.map +1 -0
  4. package/dist/index.d.ts +378 -135
  5. package/dist/index.js +3956 -1042
  6. package/dist/index.js.map +1 -1
  7. package/dist/style.css +48 -987
  8. package/package.json +7 -4
  9. package/src/adapter.ts +10 -70
  10. package/src/components/ChatPanel.tsx +373 -117
  11. package/src/components/common/ConfirmDialog.css +136 -0
  12. package/src/components/common/ConfirmDialog.tsx +91 -0
  13. package/src/components/common/CopyButton.css +22 -0
  14. package/src/components/common/CopyButton.tsx +46 -0
  15. package/src/components/common/IndexingSettings.css +207 -0
  16. package/src/components/common/IndexingSettings.tsx +398 -0
  17. package/src/components/common/SettingsPanel.css +256 -0
  18. package/src/components/common/SettingsPanel.tsx +120 -0
  19. package/src/components/common/Toast.css +50 -0
  20. package/src/components/common/Toast.tsx +38 -0
  21. package/src/components/common/ToggleSwitch.css +52 -0
  22. package/src/components/common/ToggleSwitch.tsx +20 -0
  23. package/src/components/header/ChatHeader.css +285 -0
  24. package/src/components/header/ChatHeader.tsx +376 -0
  25. package/src/components/input/AtFilePicker.css +147 -0
  26. package/src/components/input/AtFilePicker.tsx +519 -0
  27. package/src/components/input/ChatInput.css +204 -0
  28. package/src/components/input/ChatInput.tsx +506 -0
  29. package/src/components/input/DropdownSelector.css +159 -0
  30. package/src/components/input/DropdownSelector.tsx +195 -0
  31. package/src/components/input/ImagePreviewModal.css +124 -0
  32. package/src/components/input/ImagePreviewModal.tsx +118 -0
  33. package/src/components/input/at-views/AtBranchView.tsx +34 -0
  34. package/src/components/input/at-views/AtBrowserView.tsx +34 -0
  35. package/src/components/input/at-views/AtChatsView.tsx +34 -0
  36. package/src/components/input/at-views/AtDocsView.tsx +34 -0
  37. package/src/components/input/at-views/AtFilesView.tsx +168 -0
  38. package/src/components/input/at-views/AtTerminalsView.tsx +34 -0
  39. package/src/components/input/at-views/AtViewStyles.css +143 -0
  40. package/src/components/input/at-views/index.ts +9 -0
  41. package/src/components/message/ContentRenderer.css +9 -0
  42. package/src/components/message/ContentRenderer.tsx +63 -0
  43. package/src/components/message/MessageBubble.css +190 -0
  44. package/src/components/message/MessageBubble.tsx +231 -0
  45. package/src/components/message/PartsRenderer.css +4 -0
  46. package/src/components/message/PartsRenderer.tsx +114 -0
  47. package/src/components/message/ToolResultRenderer.tsx +21 -0
  48. package/src/components/message/WelcomeMessage.css +221 -0
  49. package/src/components/message/WelcomeMessage.tsx +93 -0
  50. package/src/components/message/blocks/CodeBlock.tsx +60 -0
  51. package/src/components/message/blocks/TextBlock.tsx +15 -0
  52. package/src/components/message/blocks/blocks.css +141 -0
  53. package/src/components/message/blocks/index.ts +6 -0
  54. package/src/components/message/parts/CollapsibleCard.css +78 -0
  55. package/src/components/message/parts/CollapsibleCard.tsx +77 -0
  56. package/src/components/message/parts/ErrorPart.css +9 -0
  57. package/src/components/message/parts/ErrorPart.tsx +40 -0
  58. package/src/components/message/parts/ImagePart.css +50 -0
  59. package/src/components/message/parts/ImagePart.tsx +54 -0
  60. package/src/components/message/parts/SearchPart.css +44 -0
  61. package/src/components/message/parts/SearchPart.tsx +63 -0
  62. package/src/components/message/parts/TextPart.css +10 -0
  63. package/src/components/message/parts/TextPart.tsx +20 -0
  64. package/src/components/message/parts/ThinkingPart.css +9 -0
  65. package/src/components/message/parts/ThinkingPart.tsx +48 -0
  66. package/src/components/message/parts/ToolCallPart.css +220 -0
  67. package/src/components/message/parts/ToolCallPart.tsx +285 -0
  68. package/src/components/message/parts/ToolResultPart.css +68 -0
  69. package/src/components/message/parts/ToolResultPart.tsx +96 -0
  70. package/src/components/message/parts/index.ts +11 -0
  71. package/src/components/message/tool-results/DefaultToolResult.tsx +26 -0
  72. package/src/components/message/tool-results/SearchResults.tsx +69 -0
  73. package/src/components/message/tool-results/WeatherCard.tsx +63 -0
  74. package/src/components/message/tool-results/index.ts +7 -0
  75. package/src/components/message/tool-results/tool-results.css +179 -0
  76. package/src/components/message/welcome-types.ts +46 -0
  77. package/src/context/AutoRunConfigContext.tsx +13 -0
  78. package/src/context/ChatAdapterContext.tsx +8 -0
  79. package/src/context/ChatInputContext.tsx +40 -0
  80. package/src/context/RenderersContext.tsx +41 -0
  81. package/src/hooks/useChat.ts +855 -237
  82. package/src/hooks/useImageUpload.ts +253 -0
  83. package/src/index.ts +96 -39
  84. package/src/styles.css +48 -987
  85. package/src/types/index.ts +172 -103
  86. package/src/utils/fileIcon.ts +49 -0
  87. package/src/components/ChatInput.tsx +0 -368
  88. package/src/components/chat/messages/ExecutionSteps.tsx +0 -234
  89. package/src/components/chat/messages/MessageBubble.tsx +0 -130
  90. package/src/components/chat/ui/ChatHeader.tsx +0 -301
  91. package/src/components/chat/ui/WelcomeMessage.tsx +0 -107
@@ -0,0 +1,398 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import { ConfirmDialog } from './ConfirmDialog'
4
+ import './IndexingSettings.css'
5
+
6
+ export function IndexingSettings() {
7
+ // 索引状态
8
+ const [indexedFiles, setIndexedFiles] = useState(0)
9
+ const [isIndexing, setIsIndexing] = useState(false)
10
+ const [currentFile, setCurrentFile] = useState<string | null>(null)
11
+ const [progressPercentage, setProgressPercentage] = useState(100)
12
+ const [progressIndexed, setProgressIndexed] = useState(0)
13
+ const [progressTotal, setProgressTotal] = useState(0)
14
+ const [currentStage, setCurrentStage] = useState<'scanning' | 'parsing' | 'embedding' | 'storing' | 'done' | null>(null)
15
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
16
+ const [indexSize, setIndexSize] = useState('0 B')
17
+ const [lastUpdated, setLastUpdated] = useState<string | null>(null)
18
+ const cancelIndexingRef = useRef(false)
19
+
20
+ /** 格式化文件大小 */
21
+ const formatSize = useCallback((bytes: number): string => {
22
+ if (bytes < 1024) return `${bytes} B`
23
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
24
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
25
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
26
+ }, [])
27
+
28
+ /** 格式化日期 */
29
+ const formatDate = useCallback((date: Date): string => {
30
+ const now = new Date()
31
+ const diff = now.getTime() - date.getTime()
32
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24))
33
+
34
+ if (days === 0) {
35
+ const hours = Math.floor(diff / (1000 * 60 * 60))
36
+ if (hours === 0) {
37
+ const minutes = Math.floor(diff / (1000 * 60))
38
+ return minutes <= 1 ? '刚刚' : `${minutes} 分钟前`
39
+ }
40
+ return `${hours} 小时前`
41
+ }
42
+
43
+ if (days === 1) return '昨天'
44
+ if (days < 7) return `${days} 天前`
45
+ if (days < 30) return `${Math.floor(days / 7)} 周前`
46
+ if (days < 365) return `${Math.floor(days / 30)} 个月前`
47
+ return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric' })
48
+ }, [])
49
+
50
+ /** 获取索引统计信息 */
51
+ const fetchIndexStats = useCallback(async () => {
52
+ try {
53
+ const bridge = (window as any).aiChatBridge
54
+ if (bridge?.getIndexStats) {
55
+ const stats = await bridge.getIndexStats()
56
+ setIndexedFiles(stats.totalDocuments || 0)
57
+ setIndexSize(formatSize(stats.indexSize || 0))
58
+ setLastUpdated(stats.lastUpdated ? formatDate(new Date(stats.lastUpdated)) : null)
59
+ } else {
60
+ // Bridge 方法未实现,显示默认值
61
+ setIndexedFiles(0)
62
+ setIndexSize('0 B')
63
+ setLastUpdated(null)
64
+ console.warn('getIndexStats 方法未在 bridge 中实现')
65
+ }
66
+ } catch (error) {
67
+ console.error('获取索引统计失败:', error)
68
+ // 出错时显示默认值
69
+ setIndexedFiles(0)
70
+ setIndexSize('0 B')
71
+ setLastUpdated(null)
72
+ }
73
+ }, [formatSize, formatDate])
74
+
75
+
76
+ // 进度监听器清理函数引用
77
+ const progressCleanupRef = useRef<(() => void) | null>(null)
78
+
79
+ /** 设置进度监听器 */
80
+ const setupProgressListener = useCallback(() => {
81
+ // 如果已有监听器,先清理
82
+ if (progressCleanupRef.current) {
83
+ progressCleanupRef.current()
84
+ progressCleanupRef.current = null
85
+ }
86
+
87
+ const bridge = (window as any).aiChatBridge
88
+ if (!bridge?.onIndexProgress) {
89
+ return
90
+ }
91
+
92
+ // 订阅进度更新(始终监听,即使没有主动触发索引)
93
+ progressCleanupRef.current = bridge.onIndexProgress((progress: {
94
+ indexed: number
95
+ total: number
96
+ currentFile?: string
97
+ stage: string
98
+ error?: string
99
+ }) => {
100
+ if (progress.error) {
101
+ console.error('索引错误:', progress.error)
102
+ setIsIndexing(false)
103
+ setCurrentFile(null)
104
+ return
105
+ }
106
+
107
+ if (progress.stage === 'cancelled') {
108
+ setIsIndexing(false)
109
+ setCurrentStage(null)
110
+ setCurrentFile(null)
111
+ cancelIndexingRef.current = true
112
+ return
113
+ }
114
+
115
+ // 如果收到进度更新,说明正在索引
116
+ if (progress.stage !== 'done') {
117
+ setIsIndexing(true)
118
+ }
119
+
120
+ // 更新阶段
121
+ setCurrentStage(progress.stage as 'scanning' | 'parsing' | 'embedding' | 'storing' | 'done' | null)
122
+
123
+ // 扫描阶段:total 为 0,使用 indexed 表示已扫描的文件数
124
+ if (progress.stage === 'scanning') {
125
+ setProgressIndexed(progress.indexed)
126
+ setProgressTotal(0)
127
+ setProgressPercentage(0) // 扫描阶段不显示百分比
128
+ } else {
129
+ setProgressIndexed(progress.indexed)
130
+ setProgressTotal(progress.total)
131
+ setProgressPercentage(progress.total > 0
132
+ ? Math.round((progress.indexed / progress.total) * 100)
133
+ : 0)
134
+ }
135
+ setCurrentFile(progress.currentFile || null)
136
+
137
+ if (progress.stage === 'done') {
138
+ setIsIndexing(false)
139
+ setCurrentStage(null)
140
+ setCurrentFile(null)
141
+ // 同步完成后刷新统计
142
+ fetchIndexStats()
143
+ }
144
+ })
145
+ }, [fetchIndexStats])
146
+
147
+ /** 检查索引状态并恢复进度显示 */
148
+ const checkIndexStatus = useCallback(async () => {
149
+ const bridge = (window as any).aiChatBridge
150
+ if (!bridge?.getIndexStatus) {
151
+ return
152
+ }
153
+
154
+ try {
155
+ const status = await bridge.getIndexStatus()
156
+ if (status.isIndexing && status.lastProgress) {
157
+ // 如果有正在进行的索引任务,恢复进度显示
158
+ setIsIndexing(true)
159
+ const progress = status.lastProgress
160
+
161
+ // 恢复进度状态
162
+ setCurrentStage(progress.stage as 'scanning' | 'parsing' | 'embedding' | 'storing' | 'done' | null)
163
+
164
+ if (progress.stage === 'scanning') {
165
+ setProgressIndexed(progress.indexed)
166
+ setProgressTotal(0)
167
+ setProgressPercentage(0)
168
+ } else {
169
+ setProgressIndexed(progress.indexed)
170
+ setProgressTotal(progress.total)
171
+ setProgressPercentage(progress.total > 0
172
+ ? Math.round((progress.indexed / progress.total) * 100)
173
+ : 0)
174
+ }
175
+
176
+ setCurrentFile(progress.currentFile || null)
177
+ }
178
+ } catch (error) {
179
+ console.error('检查索引状态失败:', error)
180
+ }
181
+ }, [])
182
+
183
+ /** 同步索引 */
184
+ const handleSync = useCallback(async () => {
185
+ if (isIndexing) return
186
+
187
+ const bridge = (window as any).aiChatBridge
188
+ if (!bridge?.syncIndex) {
189
+ console.error('syncIndex 方法未在 bridge 中实现')
190
+ return
191
+ }
192
+
193
+ // 确保进度监听器已设置
194
+ if (!progressCleanupRef.current) {
195
+ setupProgressListener()
196
+ }
197
+
198
+ setIsIndexing(true)
199
+ cancelIndexingRef.current = false
200
+ setProgressPercentage(0)
201
+ setProgressIndexed(0)
202
+ setProgressTotal(0)
203
+ setCurrentStage(null)
204
+ setCurrentFile(null)
205
+
206
+ try {
207
+ await bridge.syncIndex()
208
+ } catch (error) {
209
+ if (!cancelIndexingRef.current) {
210
+ console.error('同步索引失败:', error)
211
+ }
212
+ setIsIndexing(false)
213
+ setCurrentFile(null)
214
+ }
215
+ }, [isIndexing, setupProgressListener])
216
+
217
+ /** 取消索引 */
218
+ const handleCancelIndex = useCallback(async () => {
219
+ cancelIndexingRef.current = true
220
+ setIsIndexing(false)
221
+ setCurrentFile(null)
222
+
223
+ const bridge = (window as any).aiChatBridge
224
+ if (bridge?.cancelIndex) {
225
+ try {
226
+ await bridge.cancelIndex()
227
+ } catch (error) {
228
+ console.error('取消索引失败:', error)
229
+ }
230
+ }
231
+ }, [])
232
+
233
+ /** 删除索引 */
234
+ const handleDeleteIndex = useCallback(async () => {
235
+ if (isIndexing) return
236
+
237
+ setShowDeleteConfirm(false)
238
+
239
+ const bridge = (window as any).aiChatBridge
240
+ if (!bridge?.deleteIndex) {
241
+ console.error('deleteIndex 方法未在 bridge 中实现')
242
+ return
243
+ }
244
+
245
+ try {
246
+ await bridge.deleteIndex()
247
+
248
+ // 刷新统计
249
+ await fetchIndexStats()
250
+ } catch (error) {
251
+ console.error('删除索引失败:', error)
252
+ }
253
+ }, [isIndexing, fetchIndexStats])
254
+
255
+ // 组件挂载时获取统计信息、注册监听器、检查索引状态并设置进度监听
256
+ useEffect(() => {
257
+ const init = async () => {
258
+ const bridge = (window as any).aiChatBridge
259
+
260
+ // 注册索引进度监听器
261
+ if (bridge?.registerIndexListener) {
262
+ try {
263
+ await bridge.registerIndexListener()
264
+ } catch (error) {
265
+ console.error('注册索引进度监听器失败:', error)
266
+ }
267
+ }
268
+
269
+ await fetchIndexStats()
270
+ setupProgressListener()
271
+ await checkIndexStatus()
272
+ }
273
+ init()
274
+ }, [fetchIndexStats, setupProgressListener, checkIndexStatus])
275
+
276
+ // 组件卸载时清理进度监听器
277
+ useEffect(() => {
278
+ return () => {
279
+ const bridge = (window as any).aiChatBridge
280
+
281
+ // 注销索引进度监听器
282
+ if (bridge?.unregisterIndexListener) {
283
+ try {
284
+ bridge.unregisterIndexListener()
285
+ } catch (error) {
286
+ console.error('注销索引进度监听器失败:', error)
287
+ }
288
+ }
289
+
290
+ if (progressCleanupRef.current) {
291
+ progressCleanupRef.current()
292
+ progressCleanupRef.current = null
293
+ }
294
+ }
295
+ }, [])
296
+
297
+ return (
298
+ <div className="indexing-settings">
299
+ {/* 代码库索引部分 */}
300
+ <div className="setting-item indexing-section">
301
+ <div className="setting-info">
302
+ <div className="setting-label">代码库索引</div>
303
+ <p className="setting-description">
304
+ 嵌入代码库以改进上下文理解和知识。所有数据(嵌入向量、元数据和代码)都存储在本地。
305
+ </p>
306
+ <div className="indexing-content">
307
+ {/* 进度条 */}
308
+ <div className="progress-container">
309
+ <div className="progress-bar">
310
+ <div
311
+ className="progress-fill"
312
+ style={{ width: `${progressPercentage}%` }}
313
+ ></div>
314
+ </div>
315
+ <div className="progress-text">
316
+ {isIndexing ? (
317
+ currentStage === 'scanning' ? (
318
+ <span>正在扫描文件... (已扫描 {progressIndexed.toLocaleString()} 个文件)</span>
319
+ ) : (
320
+ <span>{progressPercentage}% ({progressIndexed}/{progressTotal})</span>
321
+ )
322
+ ) : (
323
+ <span>{indexedFiles.toLocaleString()} 个文件</span>
324
+ )}
325
+ </div>
326
+ </div>
327
+
328
+ {/* 统计信息 */}
329
+ {!isIndexing && indexedFiles > 0 && (
330
+ <div className="stats-info">
331
+ <div className="stat-item">
332
+ <Icon icon="lucide:database" width={14} />
333
+ <span>索引大小: {indexSize}</span>
334
+ </div>
335
+ {lastUpdated && (
336
+ <div className="stat-item">
337
+ <Icon icon="lucide:clock" width={14} />
338
+ <span>最后更新: {lastUpdated}</span>
339
+ </div>
340
+ )}
341
+ </div>
342
+ )}
343
+
344
+ {/* 当前文件信息 */}
345
+ {isIndexing && currentFile && (
346
+ <div className="current-file">
347
+ <Icon icon="lucide:file-text" width={14} />
348
+ <span className="file-path">{currentFile}</span>
349
+ </div>
350
+ )}
351
+
352
+ {/* 操作按钮 */}
353
+ <div className="action-buttons">
354
+ {!isIndexing ? (
355
+ <button
356
+ className="edit-btn"
357
+ onClick={handleSync}
358
+ >
359
+ <Icon icon="lucide:refresh-cw" width={16} />
360
+ <span>同步</span>
361
+ </button>
362
+ ) : (
363
+ <button
364
+ className="edit-btn"
365
+ onClick={handleCancelIndex}
366
+ >
367
+ <Icon icon="lucide:x" width={16} />
368
+ <span>取消</span>
369
+ </button>
370
+ )}
371
+ <button
372
+ className="edit-btn delete-btn"
373
+ disabled={isIndexing}
374
+ onClick={() => setShowDeleteConfirm(true)}
375
+ >
376
+ <Icon icon="lucide:trash-2" width={16} />
377
+ <span>删除索引</span>
378
+ </button>
379
+ </div>
380
+ </div>
381
+ </div>
382
+ </div>
383
+
384
+ {/* 删除确认对话框 */}
385
+ <ConfirmDialog
386
+ visible={showDeleteConfirm}
387
+ type="danger"
388
+ title="删除索引"
389
+ message={`确定要删除所有索引吗?\n\n这将删除 ${indexedFiles.toLocaleString()} 个文件的索引数据(${indexSize}),此操作无法撤销。`}
390
+ confirmText="删除"
391
+ cancelText="取消"
392
+ onConfirm={handleDeleteIndex}
393
+ onCancel={() => setShowDeleteConfirm(false)}
394
+ />
395
+ </div>
396
+ )
397
+ }
398
+
@@ -0,0 +1,256 @@
1
+ .settings-panel-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background: rgba(0, 0, 0, 0.7);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 1000;
12
+ }
13
+
14
+ .settings-panel {
15
+ background: var(--chat-bg, #1e1e1e);
16
+ border: 1px solid var(--chat-border, #333);
17
+ border-radius: 12px;
18
+ width: 90%;
19
+ max-width: 900px;
20
+ height: 80vh;
21
+ max-height: 700px;
22
+ display: flex;
23
+ flex-direction: row;
24
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
25
+ overflow: hidden;
26
+ }
27
+
28
+ /* 左侧导航 - shadcn 风格 */
29
+ .settings-sidebar {
30
+ width: 240px;
31
+ background: var(--chat-muted, #2a2a2a);
32
+ border-right: 1px solid var(--chat-border, #333);
33
+ display: flex;
34
+ flex-direction: column;
35
+ flex-shrink: 0;
36
+ }
37
+
38
+ .sidebar-header {
39
+ display: flex;
40
+ align-items: center;
41
+ height: 50px;
42
+ padding: 0 16px;
43
+ border-bottom: 1px solid var(--chat-border, #333);
44
+ }
45
+
46
+ .sidebar-title {
47
+ margin: 0;
48
+ font-size: 15px;
49
+ font-weight: 600;
50
+ color: var(--chat-text, #fff);
51
+ letter-spacing: 0.2px;
52
+ line-height: 1;
53
+ }
54
+
55
+ .sidebar-content {
56
+ flex: 1;
57
+ overflow-y: auto;
58
+ padding: 8px;
59
+ }
60
+
61
+ .sidebar-item {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 12px;
65
+ width: 100%;
66
+ padding: 10px 12px;
67
+ margin-bottom: 2px;
68
+ background: transparent;
69
+ border: none;
70
+ border-radius: 8px;
71
+ font-size: 14px;
72
+ font-weight: 500;
73
+ color: var(--chat-text-muted, #888);
74
+ cursor: pointer;
75
+ transition: all 0.15s ease;
76
+ text-align: left;
77
+ position: relative;
78
+ }
79
+
80
+ .sidebar-item:hover {
81
+ background: rgba(255, 255, 255, 0.05);
82
+ color: var(--chat-text, #ccc);
83
+ }
84
+
85
+ .sidebar-item.active {
86
+ background: rgba(255, 255, 255, 0.1);
87
+ color: var(--chat-text, #fff);
88
+ }
89
+
90
+ /* 右侧内容 */
91
+ .settings-content {
92
+ flex: 1;
93
+ display: flex;
94
+ flex-direction: column;
95
+ overflow: hidden;
96
+ }
97
+
98
+ .content-header {
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: space-between;
102
+ height: 50px;
103
+ padding: 0 16px;
104
+ border-bottom: 1px solid var(--chat-border, #333);
105
+ }
106
+
107
+ .content-title {
108
+ margin: 0;
109
+ font-size: 15px;
110
+ font-weight: 600;
111
+ color: var(--chat-text, #fff);
112
+ letter-spacing: 0.2px;
113
+ line-height: 1;
114
+ }
115
+
116
+ .close-btn {
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ width: 32px;
121
+ height: 32px;
122
+ padding: 0;
123
+ background: transparent;
124
+ border: none;
125
+ border-radius: 6px;
126
+ color: var(--chat-text-muted, #888);
127
+ cursor: pointer;
128
+ transition: all 0.15s;
129
+ }
130
+
131
+ .close-btn:hover {
132
+ background: var(--chat-muted, #2a2a2a);
133
+ color: var(--chat-text, #fff);
134
+ }
135
+
136
+ .content-body {
137
+ flex: 1;
138
+ overflow-y: auto;
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 24px;
142
+ padding: 0 16px;
143
+ }
144
+
145
+ .setting-section {
146
+ }
147
+
148
+ .section-title {
149
+ font-size: 16px;
150
+ font-weight: 500;
151
+ color: var(--chat-text, #fff);
152
+ margin-bottom: 4px;
153
+ }
154
+
155
+ .section-description {
156
+ font-size: 13px;
157
+ color: var(--chat-text-muted, #888);
158
+ margin-bottom: 16px;
159
+ }
160
+
161
+ .setting-item {
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: space-between;
165
+ padding: 16px 0;
166
+ border-bottom: 1px solid var(--chat-border, #333);
167
+ }
168
+
169
+ .setting-item:last-child {
170
+ border-bottom: none;
171
+ }
172
+
173
+ .setting-info {
174
+ flex: 1;
175
+ margin-right: 24px;
176
+ }
177
+
178
+ .setting-label {
179
+ font-size: 14px;
180
+ font-weight: 500;
181
+ color: var(--chat-text, #fff);
182
+ margin-bottom: 4px;
183
+ }
184
+
185
+ .setting-description {
186
+ font-size: 13px;
187
+ color: var(--chat-text-muted, #888);
188
+ line-height: 1.5;
189
+ }
190
+
191
+ .setting-control {
192
+ flex-shrink: 0;
193
+ }
194
+
195
+ .dropdown-wrapper {
196
+ /* min-width: 280px; */
197
+ }
198
+
199
+ .dropdown-wrapper .dropdown-selector .selector-text {
200
+ max-width: none;
201
+ white-space: nowrap;
202
+ }
203
+
204
+ /* 开关样式 */
205
+ .toggle-switch {
206
+ position: relative;
207
+ display: inline-block;
208
+ width: 44px;
209
+ height: 24px;
210
+ cursor: pointer;
211
+ }
212
+
213
+ .toggle-switch input {
214
+ opacity: 0;
215
+ width: 0;
216
+ height: 0;
217
+ }
218
+
219
+ .toggle-slider {
220
+ position: absolute;
221
+ top: 0;
222
+ left: 0;
223
+ right: 0;
224
+ bottom: 0;
225
+ background-color: var(--chat-muted, #3c3c3c);
226
+ border-radius: 24px;
227
+ transition: all 0.2s;
228
+ border: 1px solid rgba(255, 255, 255, 0.1);
229
+ }
230
+
231
+ .toggle-slider:before {
232
+ position: absolute;
233
+ content: "";
234
+ height: 18px;
235
+ width: 18px;
236
+ left: 3px;
237
+ top: 50%;
238
+ transform: translateY(-50%);
239
+ background-color: #fff;
240
+ border-radius: 50%;
241
+ transition: all 0.2s;
242
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
243
+ }
244
+
245
+ .toggle-switch input:checked + .toggle-slider {
246
+ background-color: rgb(153, 207, 140);
247
+ border-color: rgba(255, 255, 255, 0.2);
248
+ }
249
+
250
+ .toggle-switch input:checked + .toggle-slider:before {
251
+ transform: translate(20px, -50%);
252
+ }
253
+
254
+ .toggle-switch input:focus + .toggle-slider {
255
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
256
+ }