@huyooo/ai-chat-frontend-react 0.1.2

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.
@@ -0,0 +1,301 @@
1
+ /**
2
+ * ChatHeader Component
3
+ * 与 Vue 版本 ChatHeader.vue 保持一致
4
+ */
5
+
6
+ import { useState, useRef, useEffect, useCallback, type FC } from 'react'
7
+ import { Plus, Clock, MoreHorizontal, X, MessageSquare, Pencil, Trash2 } from 'lucide-react'
8
+ import type { SessionRecord } from '../../../types'
9
+
10
+ interface ChatHeaderProps {
11
+ /** 当前会话列表 */
12
+ sessions: SessionRecord[]
13
+ /** 当前会话 ID */
14
+ currentSessionId?: string | null
15
+ /** 是否显示关闭按钮 */
16
+ showClose?: boolean
17
+ /** 创建新会话 */
18
+ onNewSession?: () => void
19
+ /** 切换会话 */
20
+ onSwitchSession?: (sessionId: string) => void
21
+ /** 删除会话 */
22
+ onDeleteSession?: (sessionId: string) => void
23
+ /** 关闭面板 */
24
+ onClose?: () => void
25
+ /** 清空所有对话 */
26
+ onClearAll?: () => void
27
+ /** 关闭其他对话 */
28
+ onCloseOthers?: () => void
29
+ /** 导出对话 */
30
+ onExport?: () => void
31
+ /** 复制请求 ID */
32
+ onCopyId?: () => void
33
+ /** 反馈 */
34
+ onFeedback?: () => void
35
+ /** Agent 设置 */
36
+ onSettings?: () => void
37
+ }
38
+
39
+ /** 格式化时间 */
40
+ function formatTime(date: Date | string | undefined): string {
41
+ if (!date) return ''
42
+ const d = new Date(date)
43
+ const now = new Date()
44
+ const diff = now.getTime() - d.getTime()
45
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24))
46
+
47
+ if (days === 0) {
48
+ return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
49
+ } else if (days === 1) {
50
+ return '昨天'
51
+ } else if (days < 7) {
52
+ return `${days}天前`
53
+ } else {
54
+ return d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
55
+ }
56
+ }
57
+
58
+ export const ChatHeader: FC<ChatHeaderProps> = ({
59
+ sessions,
60
+ currentSessionId,
61
+ showClose = false,
62
+ onNewSession,
63
+ onSwitchSession,
64
+ onDeleteSession,
65
+ onClose,
66
+ onClearAll,
67
+ onCloseOthers,
68
+ onExport,
69
+ onCopyId,
70
+ onFeedback,
71
+ onSettings,
72
+ }) => {
73
+ const [historyOpen, setHistoryOpen] = useState(false)
74
+ const [moreMenuOpen, setMoreMenuOpen] = useState(false)
75
+ const [hiddenTabs, setHiddenTabs] = useState<Set<string>>(new Set())
76
+
77
+ const historyRef = useRef<HTMLDivElement>(null)
78
+ const moreMenuRef = useRef<HTMLDivElement>(null)
79
+
80
+ // 可见的会话
81
+ const visibleSessions = sessions.filter((s) => !hiddenTabs.has(s.id))
82
+
83
+ // 点击外部关闭菜单
84
+ useEffect(() => {
85
+ const handleClickOutside = (event: MouseEvent) => {
86
+ const target = event.target as HTMLElement
87
+ if (historyRef.current && !historyRef.current.contains(target)) {
88
+ setHistoryOpen(false)
89
+ }
90
+ if (moreMenuRef.current && !moreMenuRef.current.contains(target)) {
91
+ setMoreMenuOpen(false)
92
+ }
93
+ }
94
+
95
+ document.addEventListener('click', handleClickOutside)
96
+ return () => document.removeEventListener('click', handleClickOutside)
97
+ }, [])
98
+
99
+ // 切换会话
100
+ const handleSwitchSession = useCallback(
101
+ (sessionId: string) => {
102
+ onSwitchSession?.(sessionId)
103
+ setHistoryOpen(false)
104
+ },
105
+ [onSwitchSession]
106
+ )
107
+
108
+ // 隐藏 tab
109
+ const handleHideTab = useCallback(
110
+ (sessionId: string, e: React.MouseEvent) => {
111
+ e.stopPropagation()
112
+ setHiddenTabs((prev) => new Set([...prev, sessionId]))
113
+ if (sessionId === currentSessionId) {
114
+ const remaining = sessions.filter((s) => s.id !== sessionId && !hiddenTabs.has(s.id))
115
+ if (remaining.length > 0) {
116
+ onSwitchSession?.(remaining[0].id)
117
+ }
118
+ }
119
+ },
120
+ [currentSessionId, sessions, hiddenTabs, onSwitchSession]
121
+ )
122
+
123
+ // 删除会话
124
+ const handleDeleteSession = (sessionId: string, e: React.MouseEvent) => {
125
+ e.stopPropagation()
126
+ if (window.confirm('确定要删除这个对话吗?')) {
127
+ onDeleteSession?.(sessionId)
128
+ }
129
+ }
130
+
131
+ // 菜单项点击
132
+ const handleMenuClick = (callback?: () => void) => {
133
+ callback?.()
134
+ setMoreMenuOpen(false)
135
+ }
136
+
137
+ return (
138
+ <div className="chat-header">
139
+ {/* 左侧:Tabs */}
140
+ <div className="chat-tabs">
141
+ {visibleSessions.length === 0 ? (
142
+ <span className="chat-tab active">
143
+ <span className="chat-tab-title">New Chat</span>
144
+ </span>
145
+ ) : (
146
+ visibleSessions.map((session) => {
147
+ const title = session.title === '新对话' ? 'New Chat' : session.title
148
+ const isActive = session.id === currentSessionId
149
+ return (
150
+ <div
151
+ key={session.id}
152
+ className={`chat-tab${isActive ? ' active' : ''}`}
153
+ onClick={() => handleSwitchSession(session.id)}
154
+ title={session.title}
155
+ >
156
+ <span className="chat-tab-title">{title}</span>
157
+ <span
158
+ className="chat-tab-close"
159
+ onClick={(e) => handleHideTab(session.id, e)}
160
+ title="关闭标签"
161
+ >
162
+ <X size={12} />
163
+ </span>
164
+ </div>
165
+ )
166
+ })
167
+ )}
168
+ </div>
169
+
170
+ {/* 右侧:操作按钮 */}
171
+ <div className="chat-header-actions">
172
+ {/* 新建会话 */}
173
+ <button className="header-btn" onClick={onNewSession} title="新建对话">
174
+ <Plus size={14} />
175
+ </button>
176
+
177
+ {/* 历史记录 */}
178
+ <div ref={historyRef} style={{ position: 'relative' }}>
179
+ <button
180
+ className={`header-btn${historyOpen ? ' active' : ''}`}
181
+ onClick={(e) => {
182
+ e.stopPropagation()
183
+ setHistoryOpen(!historyOpen)
184
+ setMoreMenuOpen(false)
185
+ }}
186
+ title="历史记录"
187
+ >
188
+ <Clock size={14} />
189
+ </button>
190
+
191
+ {/* 历史记录面板 */}
192
+ {historyOpen && (
193
+ <div className="history-panel">
194
+ {sessions.length === 0 ? (
195
+ <div className="history-empty">暂无历史对话</div>
196
+ ) : (
197
+ sessions.map((session) => {
198
+ const isCurrent = session.id === currentSessionId
199
+ return (
200
+ <div
201
+ key={session.id}
202
+ className={`history-item${isCurrent ? ' active' : ''}`}
203
+ >
204
+ <button
205
+ className="history-item-content"
206
+ onClick={() => handleSwitchSession(session.id)}
207
+ >
208
+ <MessageSquare size={12} />
209
+ <span className="history-item-title">
210
+ {session.title === '新对话' ? 'New Chat' : session.title}
211
+ </span>
212
+ <span className="history-item-time">
213
+ {isCurrent ? 'Current' : formatTime(session.updatedAt)}
214
+ </span>
215
+ </button>
216
+ <div className="history-item-actions">
217
+ <button className="history-action-btn" title="编辑">
218
+ <Pencil size={10} />
219
+ </button>
220
+ <button
221
+ className="history-action-btn delete"
222
+ title="删除"
223
+ onClick={(e) => handleDeleteSession(session.id, e)}
224
+ >
225
+ <Trash2 size={10} />
226
+ </button>
227
+ </div>
228
+ </div>
229
+ )
230
+ })
231
+ )}
232
+ </div>
233
+ )}
234
+ </div>
235
+
236
+ {/* 更多选项 */}
237
+ <div ref={moreMenuRef} style={{ position: 'relative' }}>
238
+ <button
239
+ className={`header-btn${moreMenuOpen ? ' active' : ''}`}
240
+ onClick={(e) => {
241
+ e.stopPropagation()
242
+ setMoreMenuOpen(!moreMenuOpen)
243
+ setHistoryOpen(false)
244
+ }}
245
+ title="更多选项"
246
+ >
247
+ <MoreHorizontal size={14} />
248
+ </button>
249
+
250
+ {/* 更多选项菜单 */}
251
+ {moreMenuOpen && (
252
+ <div className="more-menu">
253
+ {showClose && (
254
+ <button className="menu-item" onClick={() => handleMenuClick(onClose)}>
255
+ <span>关闭对话</span>
256
+ <span className="menu-shortcut">⌘ W</span>
257
+ </button>
258
+ )}
259
+ {onClearAll && (
260
+ <button className="menu-item" onClick={() => handleMenuClick(onClearAll)}>
261
+ 清空所有对话
262
+ </button>
263
+ )}
264
+ {onCloseOthers && (
265
+ <button className="menu-item" onClick={() => handleMenuClick(onCloseOthers)}>
266
+ 关闭其他对话
267
+ </button>
268
+ )}
269
+
270
+ {(showClose || onClearAll || onCloseOthers) && <div className="menu-divider" />}
271
+
272
+ {onExport && (
273
+ <button className="menu-item" onClick={() => handleMenuClick(onExport)}>
274
+ 导出对话
275
+ </button>
276
+ )}
277
+ {onCopyId && (
278
+ <button className="menu-item" onClick={() => handleMenuClick(onCopyId)}>
279
+ 复制请求 ID
280
+ </button>
281
+ )}
282
+ {onFeedback && (
283
+ <button className="menu-item" onClick={() => handleMenuClick(onFeedback)}>
284
+ 反馈
285
+ </button>
286
+ )}
287
+
288
+ {(onExport || onCopyId || onFeedback) && onSettings && <div className="menu-divider" />}
289
+
290
+ {onSettings && (
291
+ <button className="menu-item" onClick={() => handleMenuClick(onSettings)}>
292
+ Agent 设置
293
+ </button>
294
+ )}
295
+ </div>
296
+ )}
297
+ </div>
298
+ </div>
299
+ </div>
300
+ )
301
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * WelcomeMessage Component
3
+ * 与 Vue 版本 WelcomeMessage.vue 保持一致
4
+ */
5
+
6
+ import { type FC } from 'react'
7
+ import { Wand2, ImageIcon, Video, Terminal } from 'lucide-react'
8
+
9
+ interface WelcomeMessageProps {
10
+ /** 快捷操作回调 */
11
+ onQuickAction: (text: string) => void
12
+ }
13
+
14
+ /** 快捷操作配置 */
15
+ const QUICK_ACTIONS = [
16
+ {
17
+ id: 'txt2img',
18
+ Icon: Wand2,
19
+ label: '文生图',
20
+ desc: 'AI 绘制创意图像',
21
+ prompt: '帮我生成一张图片:',
22
+ gradient: 'purple',
23
+ iconColor: 'purple',
24
+ featured: true,
25
+ },
26
+ {
27
+ id: 'img2img',
28
+ Icon: ImageIcon,
29
+ label: '图生图',
30
+ desc: '风格迁移',
31
+ prompt: '基于这张图片进行风格转换',
32
+ gradient: 'blue',
33
+ iconColor: 'blue',
34
+ featured: false,
35
+ },
36
+ {
37
+ id: 'img2vid',
38
+ Icon: Video,
39
+ label: '图生视频',
40
+ desc: '动态化',
41
+ prompt: '将这张图片转换成视频',
42
+ gradient: 'emerald',
43
+ iconColor: 'emerald',
44
+ featured: false,
45
+ },
46
+ {
47
+ id: 'cmd',
48
+ Icon: Terminal,
49
+ label: '执行命令',
50
+ desc: '系统管理',
51
+ prompt: '执行命令:',
52
+ gradient: 'orange',
53
+ iconColor: 'orange',
54
+ featured: true,
55
+ },
56
+ ]
57
+
58
+ export const WelcomeMessage: FC<WelcomeMessageProps> = ({ onQuickAction }) => {
59
+ return (
60
+ <div className="welcome-message">
61
+ {/* 动态极光背景 */}
62
+ <div className="welcome-glow purple" />
63
+ <div className="welcome-glow blue" />
64
+
65
+ {/* 标题区域 */}
66
+ <div className="welcome-title-area">
67
+ <h1 className="welcome-title">
68
+ Create
69
+ <br />
70
+ <span className="welcome-title-accent">Everything</span>
71
+ </h1>
72
+ <p className="welcome-subtitle">释放 AI 的无限创造力</p>
73
+ </div>
74
+
75
+ {/* 快捷操作网格 */}
76
+ <div className="quick-actions">
77
+ {QUICK_ACTIONS.map((action) => (
78
+ <button
79
+ key={action.id}
80
+ className={`quick-action-btn${action.featured ? ' featured' : ''}`}
81
+ onClick={() => onQuickAction(action.prompt)}
82
+ >
83
+ {/* 卡片背景渐变 */}
84
+ <div className={`quick-action-gradient ${action.gradient}`} />
85
+
86
+ {/* 图标 */}
87
+ <action.Icon className={`quick-action-icon ${action.iconColor}`} />
88
+
89
+ {/* 文字 */}
90
+ <div className="quick-action-text">
91
+ <span className="quick-action-label">{action.label}</span>
92
+ <span className="quick-action-desc">{action.desc}</span>
93
+ </div>
94
+
95
+ {/* 装饰性光斑 */}
96
+ <div className="quick-action-glow" />
97
+ </button>
98
+ ))}
99
+ </div>
100
+
101
+ {/* 底部装饰 */}
102
+ <div className="welcome-footer">
103
+ <div className="welcome-footer-line" />
104
+ </div>
105
+ </div>
106
+ )
107
+ }