@huyooo/ai-chat-frontend-react 0.1.6 → 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 +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,376 @@
1
+ /**
2
+ * ChatHeader Component
3
+ * 与 Vue 版本 ChatHeader.vue 保持一致
4
+ */
5
+
6
+ import { useState, useRef, useEffect, useCallback, type FC } from 'react'
7
+ import './ChatHeader.css'
8
+ import { Icon } from '@iconify/react'
9
+ import type { SessionRecord } from '../../types'
10
+
11
+ interface ChatHeaderProps {
12
+ /** 当前会话列表 */
13
+ sessions: SessionRecord[]
14
+ /** 当前会话 ID */
15
+ currentSessionId?: string | null
16
+ /** 是否显示关闭按钮 */
17
+ showClose?: boolean
18
+ /** 创建新会话 */
19
+ onNewSession?: () => void
20
+ /** 切换会话 */
21
+ onSwitchSession?: (sessionId: string) => void
22
+ /** 删除会话 */
23
+ onDeleteSession?: (sessionId: string) => void
24
+ /** 隐藏/显示会话(在 tab 栏关闭但不删除) */
25
+ onHideSession?: (sessionId: string, hidden: boolean) => void
26
+ /** 关闭面板 */
27
+ onClose?: () => void
28
+ /** 清空所有对话 */
29
+ onClearAll?: () => void
30
+ /** 关闭其他对话 */
31
+ onCloseOthers?: () => void
32
+ /** 导出对话 */
33
+ onExport?: () => void
34
+ /** 复制请求 ID */
35
+ onCopyId?: () => void
36
+ /** 反馈 */
37
+ onFeedback?: () => void
38
+ /** Agent 设置 */
39
+ onSettings?: () => void
40
+ }
41
+
42
+ /** 获取日期分组标签 */
43
+ function getDateGroupLabel(date: Date | string): string {
44
+ const d = new Date(date)
45
+ const now = new Date()
46
+
47
+ // 重置时间部分,只比较日期
48
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
49
+ const target = new Date(d.getFullYear(), d.getMonth(), d.getDate())
50
+ const diff = today.getTime() - target.getTime()
51
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24))
52
+
53
+ if (days === 0) return '今天'
54
+ if (days === 1) return '昨天'
55
+
56
+ // 其他显示具体日期
57
+ const month = d.getMonth() + 1
58
+ const day = d.getDate()
59
+ return `${month}月${day}日`
60
+ }
61
+
62
+ /** 格式化时间(HH:mm) */
63
+ function formatTime(date: Date | string): string {
64
+ const d = new Date(date)
65
+ const hours = String(d.getHours()).padStart(2, '0')
66
+ const minutes = String(d.getMinutes()).padStart(2, '0')
67
+ return `${hours}:${minutes}`
68
+ }
69
+
70
+ /** 按日期分组会话 */
71
+ interface SessionGroup {
72
+ label: string
73
+ sessions: SessionRecord[]
74
+ }
75
+
76
+ function groupSessionsByDate(sessions: SessionRecord[]): SessionGroup[] {
77
+ const groups = new Map<string, SessionRecord[]>()
78
+
79
+ for (const session of sessions) {
80
+ const label = getDateGroupLabel(session.updatedAt)
81
+ const group = groups.get(label)
82
+ if (group) {
83
+ group.push(session)
84
+ } else {
85
+ groups.set(label, [session])
86
+ }
87
+ }
88
+
89
+ return Array.from(groups.entries()).map(([label, groupSessions]) => ({
90
+ label,
91
+ sessions: groupSessions,
92
+ }))
93
+ }
94
+
95
+ /** 获取显示标题 */
96
+ function getDisplayTitle(title: string): string {
97
+ return title === '新对话' ? 'New Chat' : title
98
+ }
99
+
100
+ export const ChatHeader: FC<ChatHeaderProps> = ({
101
+ sessions,
102
+ currentSessionId,
103
+ showClose = false,
104
+ onNewSession,
105
+ onSwitchSession,
106
+ onDeleteSession,
107
+ onHideSession,
108
+ onClose,
109
+ onClearAll,
110
+ onCloseOthers,
111
+ onExport,
112
+ onCopyId,
113
+ onFeedback,
114
+ onSettings,
115
+ }) => {
116
+ const [historyOpen, setHistoryOpen] = useState(false)
117
+ const [moreMenuOpen, setMoreMenuOpen] = useState(false)
118
+
119
+ const historyRef = useRef<HTMLDivElement>(null)
120
+ const moreMenuRef = useRef<HTMLDivElement>(null)
121
+
122
+ // 可见的会话(使用 sessions 的 hidden 字段)
123
+ const visibleSessions = sessions.filter((s) => !s.hidden)
124
+
125
+ // 点击外部关闭菜单
126
+ useEffect(() => {
127
+ const handleClickOutside = (event: MouseEvent) => {
128
+ const target = event.target as HTMLElement
129
+ if (historyRef.current && !historyRef.current.contains(target)) {
130
+ setHistoryOpen(false)
131
+ }
132
+ if (moreMenuRef.current && !moreMenuRef.current.contains(target)) {
133
+ setMoreMenuOpen(false)
134
+ }
135
+ }
136
+
137
+ document.addEventListener('click', handleClickOutside)
138
+ return () => document.removeEventListener('click', handleClickOutside)
139
+ }, [])
140
+
141
+ // 切换历史面板
142
+ const toggleHistory = useCallback(() => {
143
+ setHistoryOpen((prev) => !prev)
144
+ setMoreMenuOpen(false)
145
+ }, [])
146
+
147
+ // 切换更多菜单
148
+ const toggleMore = useCallback(() => {
149
+ setMoreMenuOpen((prev) => !prev)
150
+ setHistoryOpen(false)
151
+ }, [])
152
+
153
+ // 从历史新建
154
+ const handleNewFromHistory = useCallback(() => {
155
+ onNewSession?.()
156
+ setHistoryOpen(false)
157
+ }, [onNewSession])
158
+
159
+ // 选择历史
160
+ const handleSelectHistory = useCallback(
161
+ (sessionId: string) => {
162
+ const session = sessions.find((s) => s.id === sessionId)
163
+ // 如果被隐藏了,恢复显示
164
+ if (session?.hidden) {
165
+ onHideSession?.(sessionId, false)
166
+ }
167
+ onSwitchSession?.(sessionId)
168
+ setHistoryOpen(false)
169
+ },
170
+ [sessions, onSwitchSession, onHideSession]
171
+ )
172
+
173
+ // 隐藏 tab(触发回调由父组件持久化到数据库)
174
+ const handleHideTab = useCallback(
175
+ (sessionId: string, e: React.MouseEvent) => {
176
+ e.stopPropagation()
177
+ onHideSession?.(sessionId, true)
178
+ if (sessionId === currentSessionId) {
179
+ const remaining = sessions.filter((s) => s.id !== sessionId && !s.hidden)
180
+ if (remaining.length > 0) {
181
+ onSwitchSession?.(remaining[0].id)
182
+ } else {
183
+ // 最后一个 tab 被关闭,创建新会话
184
+ onNewSession?.()
185
+ }
186
+ }
187
+ },
188
+ [currentSessionId, sessions, onSwitchSession, onNewSession, onHideSession]
189
+ )
190
+
191
+ // 删除会话
192
+ const handleDeleteSession = (sessionId: string, e: React.MouseEvent) => {
193
+ e.stopPropagation()
194
+ onDeleteSession?.(sessionId)
195
+ }
196
+
197
+ // 菜单操作
198
+ const handleMenuAction = (action: string) => {
199
+ setMoreMenuOpen(false)
200
+ switch (action) {
201
+ case 'clear-all':
202
+ onClearAll?.()
203
+ break
204
+ case 'close-others':
205
+ onCloseOthers?.()
206
+ break
207
+ case 'export':
208
+ onExport?.()
209
+ break
210
+ case 'copy-id':
211
+ onCopyId?.()
212
+ break
213
+ case 'feedback':
214
+ onFeedback?.()
215
+ break
216
+ case 'settings':
217
+ onSettings?.()
218
+ break
219
+ }
220
+ }
221
+
222
+ return (
223
+ <div className="chat-header">
224
+ {/* 左侧:Tabs 可滚动区域 */}
225
+ <div className="tabs-container">
226
+ {visibleSessions.length === 0 ? (
227
+ <span className="tab-item active">New Chat</span>
228
+ ) : (
229
+ visibleSessions.map((session) => {
230
+ const isActive = session.id === currentSessionId
231
+ return (
232
+ <div
233
+ key={session.id}
234
+ className={`tab-item${isActive ? ' active' : ''}`}
235
+ onClick={() => onSwitchSession?.(session.id)}
236
+ title={session.title}
237
+ >
238
+ <span className="tab-title">{getDisplayTitle(session.title)}</span>
239
+ <button
240
+ className="tab-close"
241
+ onClick={(e) => handleHideTab(session.id, e)}
242
+ title="关闭标签"
243
+ >
244
+ <Icon icon="lucide:x" width={18} />
245
+ </button>
246
+ </div>
247
+ )
248
+ })
249
+ )}
250
+ </div>
251
+
252
+ {/* 右侧:操作按钮 */}
253
+ <div className="header-actions">
254
+ {/* 新建会话 */}
255
+ <button className="icon-btn" onClick={onNewSession} title="新建对话">
256
+ <Icon icon="lucide:plus" width={18} />
257
+ </button>
258
+
259
+ {/* 历史记录 */}
260
+ <div ref={historyRef} className="dropdown-container">
261
+ <button
262
+ className={`icon-btn${historyOpen ? ' active' : ''}`}
263
+ onClick={(e) => {
264
+ e.stopPropagation()
265
+ toggleHistory()
266
+ }}
267
+ title="历史记录"
268
+ >
269
+ <Icon icon="lucide:clock" width={18} />
270
+ </button>
271
+
272
+ {/* 历史记录面板 */}
273
+ {historyOpen && (
274
+ <div className="dropdown-panel history-panel">
275
+ <div className="panel-header">
276
+ <span>历史记录</span>
277
+ <button className="icon-btn small" title="新建对话" onClick={handleNewFromHistory}>
278
+ <Icon icon="lucide:plus" width={18} />
279
+ </button>
280
+ </div>
281
+ <div className="panel-content chat-scrollbar">
282
+ {sessions.length === 0 ? (
283
+ <div className="empty-state">暂无历史对话</div>
284
+ ) : (
285
+ groupSessionsByDate(sessions).map((group) => (
286
+ <div key={group.label}>
287
+ <div className="history-group-label">{group.label}</div>
288
+ {group.sessions.map((session) => {
289
+ const isCurrent = session.id === currentSessionId
290
+ return (
291
+ <div
292
+ key={session.id}
293
+ className={`history-item${isCurrent ? ' active' : ''}`}
294
+ onClick={() => handleSelectHistory(session.id)}
295
+ >
296
+ <span className="history-title">{session.title}</span>
297
+ <span className="history-time">{formatTime(session.updatedAt)}</span>
298
+ <button
299
+ className="history-action-btn delete"
300
+ title="删除"
301
+ onClick={(e) => handleDeleteSession(session.id, e)}
302
+ >
303
+ <Icon icon="lucide:trash-2" width={18} />
304
+ </button>
305
+ </div>
306
+ )
307
+ })}
308
+ </div>
309
+ ))
310
+ )}
311
+ </div>
312
+ </div>
313
+ )}
314
+ </div>
315
+
316
+ {/* 更多选项 */}
317
+ <div ref={moreMenuRef} className="dropdown-container">
318
+ <button
319
+ className={`icon-btn${moreMenuOpen ? ' active' : ''}`}
320
+ onClick={(e) => {
321
+ e.stopPropagation()
322
+ toggleMore()
323
+ }}
324
+ title="更多选项"
325
+ >
326
+ <Icon icon="lucide:more-horizontal" width={18} />
327
+ </button>
328
+
329
+ {/* 更多选项菜单 */}
330
+ {moreMenuOpen && (
331
+ <div className="dropdown-panel more-panel">
332
+ <button className="menu-item" onClick={() => handleMenuAction('clear-all')}>
333
+ <Icon icon="lucide:trash-2" width={18} />
334
+ <span>清空所有对话</span>
335
+ </button>
336
+ <button className="menu-item" onClick={() => handleMenuAction('close-others')}>
337
+ <Icon icon="lucide:x-circle" width={18} />
338
+ <span>关闭其他对话</span>
339
+ </button>
340
+ <div className="menu-divider" />
341
+ <button className="menu-item" onClick={() => handleMenuAction('export')}>
342
+ <Icon icon="lucide:download" width={18} />
343
+ <span>导出对话</span>
344
+ </button>
345
+ <button className="menu-item" onClick={() => handleMenuAction('copy-id')}>
346
+ <Icon icon="lucide:copy" width={18} />
347
+ <span>复制请求 ID</span>
348
+ </button>
349
+ <div className="menu-divider" />
350
+ <button className="menu-item" onClick={() => handleMenuAction('feedback')}>
351
+ <Icon icon="lucide:message-square" width={18} />
352
+ <span>反馈</span>
353
+ </button>
354
+ <button className="menu-item" onClick={() => handleMenuAction('settings')}>
355
+ <Icon icon="lucide:settings" width={18} />
356
+ <span>Agent 设置</span>
357
+ </button>
358
+ </div>
359
+ )}
360
+ </div>
361
+
362
+ {/* 关闭按钮 */}
363
+ {showClose && (
364
+ <button className="icon-btn" onClick={onClose} title="关闭">
365
+ <Icon icon="lucide:x" width={18} />
366
+ </button>
367
+ )}
368
+
369
+ {/* 设置按钮 */}
370
+ <button className="icon-btn" onClick={onSettings} title="设置">
371
+ <Icon icon="lucide:settings" width={18} />
372
+ </button>
373
+ </div>
374
+ </div>
375
+ )
376
+ }
@@ -0,0 +1,147 @@
1
+ .at-picker-dropdown {
2
+ position: fixed;
3
+ width: 332px;
4
+ background: var(--chat-dropdown-bg, #252526);
5
+ border: 1px solid rgba(255, 255, 255, 0.1);
6
+ border-radius: 10px;
7
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
8
+ z-index: 99999;
9
+ display: flex;
10
+ flex-direction: column;
11
+ overflow: hidden;
12
+ }
13
+
14
+ .at-picker-header {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 8px;
18
+ padding: 10px 12px;
19
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
20
+ }
21
+
22
+ .at-picker-back {
23
+ width: 24px;
24
+ height: 24px;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ background: transparent;
29
+ border: none;
30
+ border-radius: 4px;
31
+ color: #888;
32
+ cursor: pointer;
33
+ flex-shrink: 0;
34
+ }
35
+
36
+ .at-picker-back:hover {
37
+ background: rgba(255, 255, 255, 0.08);
38
+ color: #ccc;
39
+ }
40
+
41
+ .at-picker-search-icon {
42
+ color: #666;
43
+ flex-shrink: 0;
44
+ }
45
+
46
+ .at-picker-search {
47
+ flex: 1;
48
+ background: transparent;
49
+ border: none;
50
+ outline: none;
51
+ color: #ddd;
52
+ font-size: 13px;
53
+ }
54
+
55
+ .at-picker-body {
56
+ padding: 6px;
57
+ flex: 1;
58
+ min-height: 0;
59
+ overflow-y: auto;
60
+ }
61
+
62
+ /* 滚动期间禁用鼠标悬停事件 */
63
+ .at-picker-body.is-scrolling .at-picker-item,
64
+ .at-picker-body.is-scrolling .at-view-item {
65
+ pointer-events: none;
66
+ }
67
+
68
+ .at-picker-section {
69
+ margin-bottom: 2px;
70
+ }
71
+
72
+ .at-picker-recent {
73
+ padding-bottom: 6px;
74
+ margin-bottom: 6px;
75
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
76
+ }
77
+
78
+ .at-picker-list {
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 1px;
82
+ }
83
+
84
+ .at-picker-item {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 8px;
88
+ text-align: left;
89
+ padding: 7px 10px;
90
+ border-radius: 6px;
91
+ border: 1px solid transparent;
92
+ background: transparent;
93
+ cursor: pointer;
94
+ color: #ccc;
95
+ width: 100%;
96
+ }
97
+
98
+ .at-picker-item:hover {
99
+ background: rgba(255, 255, 255, 0.06);
100
+ }
101
+
102
+ .at-picker-item.active {
103
+ background: rgba(59, 130, 246, 0.15);
104
+ border-color: rgba(59, 130, 246, 0.3);
105
+ }
106
+
107
+ .at-picker-item-icon {
108
+ color: #999;
109
+ flex-shrink: 0;
110
+ }
111
+
112
+ .at-picker-item-name {
113
+ font-size: 13px;
114
+ color: #ddd;
115
+ flex-shrink: 0;
116
+ max-width: 160px;
117
+ overflow: hidden;
118
+ text-overflow: ellipsis;
119
+ white-space: nowrap;
120
+ }
121
+
122
+ .at-picker-item-path {
123
+ font-size: 11px;
124
+ color: #555;
125
+ min-width: 0;
126
+ overflow: hidden;
127
+ text-overflow: ellipsis;
128
+ white-space: nowrap;
129
+ flex: 1;
130
+ }
131
+
132
+ .at-picker-category .at-picker-item-name {
133
+ flex: 1;
134
+ max-width: none;
135
+ }
136
+
137
+ .at-picker-chevron {
138
+ color: #555;
139
+ flex-shrink: 0;
140
+ }
141
+
142
+ .at-picker-footer {
143
+ padding: 8px 12px;
144
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
145
+ color: #555;
146
+ font-size: 11px;
147
+ }