@huyooo/ai-chat-frontend-react 0.1.4 → 0.1.8

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 +3954 -1044
  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 +99 -42
  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,34 @@
1
+ import { forwardRef, useImperativeHandle } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import './AtViewStyles.css'
4
+
5
+ export interface AtPlaceholderViewProps {
6
+ query: string
7
+ activeIndex: number
8
+ onSelect: (path: string) => void
9
+ onSetActive: (index: number) => void
10
+ onUpdateCount: (count: number) => void
11
+ }
12
+
13
+ export interface AtPlaceholderViewHandle {
14
+ getActivePath: () => string | null
15
+ confirmActive: () => void
16
+ }
17
+
18
+ export const AtDocsView = forwardRef<AtPlaceholderViewHandle, AtPlaceholderViewProps>((_, ref) => {
19
+ useImperativeHandle(ref, () => ({
20
+ getActivePath: () => null,
21
+ confirmActive: () => {},
22
+ }), [])
23
+
24
+ return (
25
+ <div className="at-placeholder-view">
26
+ <Icon icon="lucide:book-open" width={32} className="at-placeholder-icon" />
27
+ <div className="at-placeholder-title">文档</div>
28
+ <div className="at-placeholder-desc">功能待实现</div>
29
+ <div className="at-placeholder-hint">将支持引用项目文档、API 文档等</div>
30
+ </div>
31
+ )
32
+ })
33
+
34
+ AtDocsView.displayName = 'AtDocsView'
@@ -0,0 +1,168 @@
1
+ import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import type { ChatAdapter } from '../../../adapter'
4
+
5
+ interface FileInfo {
6
+ name: string
7
+ path: string
8
+ isDirectory: boolean
9
+ }
10
+ import { getFileIcon } from '../../../utils/fileIcon'
11
+ import './AtViewStyles.css'
12
+
13
+ export interface AtFilesViewProps {
14
+ adapter: ChatAdapter
15
+ initialDir?: string
16
+ query: string
17
+ activeIndex: number
18
+ onSelect: (path: string) => void
19
+ onSetActive: (index: number) => void
20
+ onUpdateCount: (count: number) => void
21
+ }
22
+
23
+ export interface AtFilesViewHandle {
24
+ getActivePath: () => string | null
25
+ confirmActive: () => void
26
+ }
27
+
28
+ export const AtFilesView = forwardRef<AtFilesViewHandle, AtFilesViewProps>(({
29
+ adapter,
30
+ initialDir,
31
+ query,
32
+ activeIndex,
33
+ onSelect,
34
+ onSetActive,
35
+ onUpdateCount,
36
+ }, ref) => {
37
+ const [loading, setLoading] = useState(false)
38
+ const [currentPath, setCurrentPath] = useState('')
39
+ const [entries, setEntries] = useState<FileInfo[]>([])
40
+ const initializedRef = useRef(false)
41
+
42
+ // 过滤后的文件列表
43
+ const filteredEntries = useMemo(() => {
44
+ const q = query.trim().toLowerCase()
45
+ if (!q) return entries
46
+ return entries.filter((f) => f.name.toLowerCase().includes(q))
47
+ }, [entries, query])
48
+
49
+ // 通知父组件条目数量变化
50
+ useEffect(() => {
51
+ onUpdateCount(filteredEntries.length)
52
+ }, [filteredEntries.length, onUpdateCount])
53
+
54
+ // 加载目录
55
+ const loadDir = useCallback(async (dir: string) => {
56
+ if (!adapter.listDir) return
57
+ setLoading(true)
58
+ try {
59
+ const resolved = (await adapter.resolvePath?.(dir)) || dir
60
+ const list = await adapter.listDir(resolved)
61
+ const sorted = [...list].sort((a, b) => {
62
+ if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1
63
+ return a.name.localeCompare(b.name)
64
+ })
65
+ setEntries(sorted)
66
+ setCurrentPath(resolved)
67
+ onSetActive(-1)
68
+ } finally {
69
+ setLoading(false)
70
+ }
71
+ }, [adapter, onSetActive])
72
+
73
+ // 返回上级目录
74
+ const goUp = useCallback(async () => {
75
+ if (!adapter.parentDir) return
76
+ const parent = await adapter.parentDir(currentPath)
77
+ if (parent && parent !== currentPath) {
78
+ await loadDir(parent)
79
+ }
80
+ }, [adapter, currentPath, loadDir])
81
+
82
+ // 返回主目录
83
+ const goHome = useCallback(async () => {
84
+ if (!adapter.homeDir) return
85
+ const home = await adapter.homeDir()
86
+ await loadDir(home)
87
+ }, [adapter, loadDir])
88
+
89
+ // 点击条目
90
+ const handleEntryClick = useCallback((f: FileInfo) => {
91
+ if (f.isDirectory) {
92
+ loadDir(f.path)
93
+ return
94
+ }
95
+ onSelect(f.path)
96
+ }, [loadDir, onSelect])
97
+
98
+ // 暴露给父组件的方法
99
+ useImperativeHandle(ref, () => ({
100
+ getActivePath: () => {
101
+ if (activeIndex < 0) return null
102
+ return filteredEntries[activeIndex]?.path || null
103
+ },
104
+ confirmActive: () => {
105
+ const entry = filteredEntries[activeIndex]
106
+ if (entry) {
107
+ handleEntryClick(entry)
108
+ }
109
+ },
110
+ }), [activeIndex, filteredEntries, handleEntryClick])
111
+
112
+ // 初始加载
113
+ useEffect(() => {
114
+ if (initializedRef.current) return
115
+ initializedRef.current = true
116
+
117
+ const init = async () => {
118
+ const dir = initialDir || (adapter.homeDir ? await adapter.homeDir() : '')
119
+ if (dir) await loadDir(dir)
120
+ }
121
+ init()
122
+ }, [adapter, initialDir, loadDir])
123
+
124
+ return (
125
+ <div className="at-files-view">
126
+ <div className="at-view-section-title">文件和文件夹</div>
127
+
128
+ {/* 路径栏 */}
129
+ <div className="at-view-pathbar">
130
+ <button className="at-view-pathbtn" title="返回上级" onClick={goUp}>
131
+ <Icon icon="lucide:arrow-up" width={12} />
132
+ </button>
133
+ <button className="at-view-pathbtn" title="主目录" onClick={goHome}>
134
+ <Icon icon="lucide:home" width={12} />
135
+ </button>
136
+ <div className="at-view-pathtext" title={currentPath || ''}>
137
+ {currentPath || '-'}
138
+ </div>
139
+ </div>
140
+
141
+ {/* 文件列表 */}
142
+ <div className="at-view-list">
143
+ {loading ? (
144
+ <div className="at-view-empty">加载中...</div>
145
+ ) : filteredEntries.length === 0 ? (
146
+ <div className="at-view-empty">
147
+ {query.trim() ? '没有找到匹配项' : '目录为空'}
148
+ </div>
149
+ ) : (
150
+ filteredEntries.map((f, i) => (
151
+ <button
152
+ key={f.path}
153
+ className={`at-view-item${activeIndex === i ? ' active' : ''}`}
154
+ onMouseEnter={() => onSetActive(i)}
155
+ onClick={() => handleEntryClick(f)}
156
+ >
157
+ <Icon icon={f.isDirectory ? 'lucide:folder' : getFileIcon(f.name)} width={14} className="at-view-item-icon" />
158
+ <span className="at-view-item-name">{f.name}</span>
159
+ <span className="at-view-item-path">{f.path}</span>
160
+ </button>
161
+ ))
162
+ )}
163
+ </div>
164
+ </div>
165
+ )
166
+ })
167
+
168
+ AtFilesView.displayName = 'AtFilesView'
@@ -0,0 +1,34 @@
1
+ import { forwardRef, useImperativeHandle } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import './AtViewStyles.css'
4
+
5
+ export interface AtPlaceholderViewProps {
6
+ query: string
7
+ activeIndex: number
8
+ onSelect: (path: string) => void
9
+ onSetActive: (index: number) => void
10
+ onUpdateCount: (count: number) => void
11
+ }
12
+
13
+ export interface AtPlaceholderViewHandle {
14
+ getActivePath: () => string | null
15
+ confirmActive: () => void
16
+ }
17
+
18
+ export const AtTerminalsView = forwardRef<AtPlaceholderViewHandle, AtPlaceholderViewProps>((_, ref) => {
19
+ useImperativeHandle(ref, () => ({
20
+ getActivePath: () => null,
21
+ confirmActive: () => {},
22
+ }), [])
23
+
24
+ return (
25
+ <div className="at-placeholder-view">
26
+ <Icon icon="lucide:terminal" width={32} className="at-placeholder-icon" />
27
+ <div className="at-placeholder-title">终端</div>
28
+ <div className="at-placeholder-desc">功能待实现</div>
29
+ <div className="at-placeholder-hint">将支持引用终端输出、命令历史等</div>
30
+ </div>
31
+ )
32
+ })
33
+
34
+ AtTerminalsView.displayName = 'AtTerminalsView'
@@ -0,0 +1,143 @@
1
+ /* 子视图共享样式 */
2
+
3
+ .at-files-view {
4
+ display: flex;
5
+ flex-direction: column;
6
+ }
7
+
8
+ .at-view-section-title {
9
+ font-size: 11px;
10
+ color: #888;
11
+ padding: 6px 8px 4px;
12
+ text-transform: uppercase;
13
+ letter-spacing: 0.5px;
14
+ }
15
+
16
+ .at-view-pathbar {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 4px;
20
+ padding: 4px 8px 8px;
21
+ }
22
+
23
+ .at-view-pathbtn {
24
+ width: 22px;
25
+ height: 22px;
26
+ border-radius: 4px;
27
+ background: transparent;
28
+ border: 1px solid rgba(255, 255, 255, 0.1);
29
+ color: #999;
30
+ cursor: pointer;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ }
35
+
36
+ .at-view-pathbtn:hover {
37
+ background: rgba(255, 255, 255, 0.06);
38
+ color: #ccc;
39
+ }
40
+
41
+ .at-view-pathtext {
42
+ flex: 1;
43
+ min-width: 0;
44
+ font-size: 11px;
45
+ color: #777;
46
+ white-space: nowrap;
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
49
+ }
50
+
51
+ .at-view-list {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 1px;
55
+ }
56
+
57
+ .at-view-item {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 8px;
61
+ text-align: left;
62
+ padding: 7px 10px;
63
+ border-radius: 6px;
64
+ border: 1px solid transparent;
65
+ background: transparent;
66
+ cursor: pointer;
67
+ color: #ccc;
68
+ width: 100%;
69
+ }
70
+
71
+ .at-view-item:hover {
72
+ background: rgba(255, 255, 255, 0.06);
73
+ }
74
+
75
+ .at-view-item.active {
76
+ background: rgba(59, 130, 246, 0.15);
77
+ border-color: rgba(59, 130, 246, 0.3);
78
+ }
79
+
80
+ .at-view-item-icon {
81
+ color: #999;
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ .at-view-item-name {
86
+ font-size: 13px;
87
+ color: #ddd;
88
+ flex-shrink: 0;
89
+ max-width: 160px;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ white-space: nowrap;
93
+ }
94
+
95
+ .at-view-item-path {
96
+ font-size: 11px;
97
+ color: #555;
98
+ min-width: 0;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ white-space: nowrap;
102
+ flex: 1;
103
+ }
104
+
105
+ .at-view-empty {
106
+ padding: 12px 10px;
107
+ color: #666;
108
+ font-size: 12px;
109
+ text-align: center;
110
+ }
111
+
112
+ /* 占位视图样式 */
113
+ .at-placeholder-view {
114
+ display: flex;
115
+ flex-direction: column;
116
+ align-items: center;
117
+ justify-content: center;
118
+ padding: 32px 16px;
119
+ text-align: center;
120
+ }
121
+
122
+ .at-placeholder-icon {
123
+ color: #555;
124
+ margin-bottom: 12px;
125
+ }
126
+
127
+ .at-placeholder-title {
128
+ font-size: 14px;
129
+ font-weight: 500;
130
+ color: #888;
131
+ margin-bottom: 8px;
132
+ }
133
+
134
+ .at-placeholder-desc {
135
+ font-size: 13px;
136
+ color: #666;
137
+ margin-bottom: 4px;
138
+ }
139
+
140
+ .at-placeholder-hint {
141
+ font-size: 11px;
142
+ color: #555;
143
+ }
@@ -0,0 +1,9 @@
1
+ export { AtFilesView } from './AtFilesView'
2
+ export type { AtFilesViewProps, AtFilesViewHandle } from './AtFilesView'
3
+
4
+ export { AtDocsView } from './AtDocsView'
5
+ export { AtTerminalsView } from './AtTerminalsView'
6
+ export { AtChatsView } from './AtChatsView'
7
+ export { AtBranchView } from './AtBranchView'
8
+ export { AtBrowserView } from './AtBrowserView'
9
+ export type { AtPlaceholderViewProps, AtPlaceholderViewHandle } from './AtDocsView'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * ContentRenderer 样式
3
+ */
4
+
5
+ .content-renderer {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 4px;
9
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 内容渲染器
3
+ * 将原始文本解析为内容块并渲染
4
+ */
5
+
6
+ import { FC, useMemo, useContext, type ComponentType } from 'react'
7
+ import { parseContent } from '@huyooo/ai-chat-shared'
8
+ import type { ContentBlock } from '@huyooo/ai-chat-shared'
9
+ import { TextBlock, CodeBlock } from './blocks'
10
+ import { BlockRenderersContext } from '../../context/RenderersContext'
11
+ import './ContentRenderer.css'
12
+
13
+ interface ContentRendererProps {
14
+ /** 原始文本内容 */
15
+ content: string
16
+ /** 预解析的块列表(可选,用于流式更新) */
17
+ blocks?: ContentBlock[]
18
+ /** 代码复制事件 */
19
+ onCodeCopy?: (code: string) => void
20
+ }
21
+
22
+ export const ContentRenderer: FC<ContentRendererProps> = ({
23
+ content,
24
+ blocks: preBlocks,
25
+ onCodeCopy,
26
+ }) => {
27
+ // 从上层获取自定义块渲染器
28
+ const customRenderers = useContext(BlockRenderersContext)
29
+
30
+ // 解析后的内容块
31
+ const blocks = useMemo(() => {
32
+ // 优先使用预解析的块
33
+ if (preBlocks?.length) {
34
+ return preBlocks
35
+ }
36
+ // 否则解析原始内容
37
+ return parseContent(content)
38
+ }, [content, preBlocks])
39
+
40
+ if (!blocks.length) return null
41
+
42
+ return (
43
+ <div className="content-renderer">
44
+ {blocks.map((block) => {
45
+ // 自定义块渲染器
46
+ const CustomRenderer = customRenderers[block.type] as ComponentType<{ block: ContentBlock }>
47
+ if (CustomRenderer) {
48
+ return <CustomRenderer key={block.id} block={block} />
49
+ }
50
+
51
+ // 内置渲染器
52
+ switch (block.type) {
53
+ case 'text':
54
+ return <TextBlock key={block.id} block={block} />
55
+ case 'code':
56
+ return <CodeBlock key={block.id} block={block} onCopy={onCodeCopy} />
57
+ default:
58
+ return null
59
+ }
60
+ })}
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * MessageBubble 组件样式
3
+ * 与 Vue 版本 MessageBubble.vue 保持一致
4
+ */
5
+
6
+ .message-bubble {
7
+ padding: 8px 0;
8
+ animation: fadeIn 0.2s ease;
9
+ }
10
+
11
+ @keyframes fadeIn {
12
+ from {
13
+ opacity: 0;
14
+ transform: translateY(4px);
15
+ }
16
+ to {
17
+ opacity: 1;
18
+ transform: translateY(0);
19
+ }
20
+ }
21
+
22
+ /* 用户消息 */
23
+ .message-bubble.user {
24
+ width: 100%;
25
+ }
26
+
27
+ .user-content {
28
+ width: 100%;
29
+ background: var(--chat-muted, #2d2d2d);
30
+ color: var(--chat-text, #ccc);
31
+ padding: 12px;
32
+ border-radius: 12px;
33
+ border: 1px solid var(--chat-border, #444);
34
+ }
35
+
36
+ .user-text {
37
+ font-size: 14px;
38
+ line-height: 1.5;
39
+ white-space: pre-wrap;
40
+ word-break: break-word;
41
+ }
42
+
43
+ .user-images {
44
+ display: flex;
45
+ gap: 8px;
46
+ margin-top: 8px;
47
+ flex-wrap: wrap;
48
+ }
49
+
50
+ .user-image {
51
+ width: 80px;
52
+ height: 80px;
53
+ object-fit: cover;
54
+ border-radius: 8px;
55
+ cursor: pointer;
56
+ transition: transform 0.15s;
57
+ }
58
+
59
+ .user-image:hover {
60
+ transform: scale(1.05);
61
+ }
62
+
63
+ /* 助手消息 */
64
+ .message-bubble.assistant {
65
+ position: relative;
66
+ }
67
+
68
+ .assistant-content {
69
+ max-width: 100%;
70
+ }
71
+
72
+ /* 加载提示 - 等待状态时 */
73
+ .loading-indicator {
74
+ position: relative;
75
+ display: flex;
76
+ align-items: center;
77
+ width: 100%;
78
+ padding: 10px 16px;
79
+ background: var(--chat-muted, #2a2a2a);
80
+ border-radius: 8px;
81
+ overflow: hidden;
82
+ margin: 8px 0;
83
+ }
84
+
85
+ .loading-text {
86
+ font-size: 13px;
87
+ color: var(--chat-text-muted, #888);
88
+ position: relative;
89
+ z-index: 1;
90
+ }
91
+
92
+ .loading-shimmer {
93
+ position: absolute;
94
+ top: 0;
95
+ left: -100%;
96
+ width: 100%;
97
+ height: 100%;
98
+ background: linear-gradient(
99
+ 90deg,
100
+ transparent 0%,
101
+ rgba(255, 255, 255, 0.08) 50%,
102
+ transparent 100%
103
+ );
104
+ animation: shimmer 1.5s ease-in-out infinite;
105
+ }
106
+
107
+ @keyframes shimmer {
108
+ 0% {
109
+ left: -100%;
110
+ }
111
+ 100% {
112
+ left: 100%;
113
+ }
114
+ }
115
+
116
+ /* 操作按钮 */
117
+ .message-actions {
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: space-between;
121
+ margin-top: 8px;
122
+ }
123
+
124
+ .message-meta {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 6px;
128
+ }
129
+
130
+ .model-name {
131
+ font-size: 11px;
132
+ color: var(--chat-text-muted, #888);
133
+ background: var(--chat-muted, #2a2a2a);
134
+ padding: 2px 8px;
135
+ border-radius: 10px;
136
+ border: 1px solid var(--chat-border, #333);
137
+ }
138
+
139
+ .mode-badge {
140
+ font-size: 10px;
141
+ color: var(--chat-text-muted, #888);
142
+ background: var(--chat-muted, #2a2a2a);
143
+ padding: 2px 6px;
144
+ border-radius: 10px;
145
+ border: 1px solid var(--chat-border, #333);
146
+ }
147
+
148
+ .action-buttons {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 4px;
152
+ }
153
+
154
+ .action-btn {
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ width: 24px;
159
+ height: 24px;
160
+ border: none;
161
+ background: transparent;
162
+ border-radius: 4px;
163
+ color: var(--chat-text-muted, #666);
164
+ cursor: pointer;
165
+ transition: all 0.15s;
166
+ }
167
+
168
+ .action-btn:hover {
169
+ background: var(--chat-muted, #3c3c3c);
170
+ color: var(--chat-text, #ccc);
171
+ }
172
+
173
+ .action-btn.copied {
174
+ color: #22c55e;
175
+ }
176
+
177
+ /* 消息时间 */
178
+ .message-time {
179
+ font-size: 12px;
180
+ color: var(--chat-text-muted, #666);
181
+ }
182
+
183
+ .user-time {
184
+ text-align: right;
185
+ margin-top: 8px;
186
+ }
187
+
188
+ .assistant-time {
189
+ margin-right: 8px;
190
+ }