@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,231 @@
1
+ /**
2
+ * MessageBubble Component
3
+ * 新架构:使用 ContentPart 数组渲染消息内容
4
+ */
5
+
6
+ import { useMemo, type FC } from 'react'
7
+ import './MessageBubble.css'
8
+ import { Icon } from '@iconify/react'
9
+ import { PartsRenderer } from './PartsRenderer'
10
+ import { ChatInput } from '../input/ChatInput'
11
+ import { useChatInputContext } from '../../context/ChatInputContext'
12
+ import type { ContentPart, TextPart, ToolCallPart, SearchPart, ThinkingPart, ModelOption } from '../../types'
13
+ import type { ChatAdapter } from '../../adapter'
14
+ import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
15
+
16
+ interface MessageBubbleProps {
17
+ role: 'user' | 'assistant'
18
+ /** 内容 parts 数组 - 新架构核心 */
19
+ parts: ContentPart[]
20
+ /** 生成此消息时使用的模型 */
21
+ model?: string
22
+ /** 生成此消息时使用的模式 (ask/agent) */
23
+ mode?: string
24
+ /** 用户上传的图片 */
25
+ images?: string[]
26
+ /** 是否正在加载 */
27
+ loading?: boolean
28
+ /** 是否已复制 */
29
+ copied?: boolean
30
+ /** 消息时间戳 */
31
+ timestamp?: Date | string | number
32
+ onCopy?: () => void
33
+ onRegenerate?: () => void
34
+ /** 编辑用户消息后重新发送 */
35
+ onSend?: (text: string) => void
36
+ /** 步骤折叠模式 */
37
+ stepsExpandedType?: 'open' | 'close' | 'auto'
38
+ /** 工具调用相关 - 通过 props 传递 */
39
+ adapter?: ChatAdapter
40
+ autoRunConfig?: AutoRunConfig
41
+ onSaveConfig?: (config: AutoRunConfig) => Promise<void>
42
+ }
43
+
44
+ /** 获取模型显示名称 */
45
+ function getModelDisplayName(modelId: string, models?: ModelOption[]): string {
46
+ // 优先从传入的 models 中查找
47
+ const found = models?.find((m) => m.modelId === modelId)
48
+ if (found) return found.displayName
49
+ // 后备:提取模型名称(前端不应该依赖后端包)
50
+ return modelId.split('/').pop() || modelId
51
+ }
52
+
53
+ /** 格式化时间显示 */
54
+ function formatTime(timestamp?: Date | string | number): string {
55
+ if (!timestamp) return ''
56
+ const date = new Date(timestamp)
57
+ const year = date.getFullYear()
58
+ const month = String(date.getMonth() + 1).padStart(2, '0')
59
+ const day = String(date.getDate()).padStart(2, '0')
60
+ const hours = String(date.getHours()).padStart(2, '0')
61
+ const minutes = String(date.getMinutes()).padStart(2, '0')
62
+ return `${year}-${month}-${day} ${hours}:${minutes}`
63
+ }
64
+
65
+ export const MessageBubble: FC<MessageBubbleProps> = ({
66
+ role,
67
+ parts,
68
+ model,
69
+ mode,
70
+ images,
71
+ loading,
72
+ copied,
73
+ timestamp,
74
+ onCopy,
75
+ onRegenerate,
76
+ onSend,
77
+ stepsExpandedType = 'auto',
78
+ adapter,
79
+ autoRunConfig,
80
+ onSaveConfig,
81
+ }) => {
82
+ const isUser = role === 'user'
83
+ const formattedTime = formatTime(timestamp)
84
+ const inputContext = useChatInputContext()
85
+
86
+ // 提取用户消息的文本内容
87
+ const userText = useMemo(() => {
88
+ return parts
89
+ .filter((p): p is TextPart => p.type === 'text')
90
+ .map(p => p.text)
91
+ .join('')
92
+ }, [parts])
93
+
94
+ // 是否有内容(用于显示操作按钮)
95
+ const hasContent = useMemo(() => {
96
+ return parts.some(p =>
97
+ (p.type === 'text' && p.text) ||
98
+ p.type === 'tool_result' ||
99
+ p.type === 'thinking' ||
100
+ p.type === 'search'
101
+ )
102
+ }, [parts])
103
+
104
+ // loading 状态:决定显示什么类型的指示器
105
+ // cursor: 闪烁光标(文本流式输出时)
106
+ // text: 文字提示(等待状态时)
107
+ // none: 不显示
108
+ const loadingState = useMemo<{ type: 'cursor' | 'text' | 'none'; text?: string }>(() => {
109
+ if (!loading) {
110
+ return { type: 'none' }
111
+ }
112
+
113
+ if (parts.length === 0) {
114
+ return { type: 'text', text: '正在思考...' }
115
+ }
116
+
117
+ const lastPart = parts[parts.length - 1]
118
+
119
+ // 文本流式输出 → 不需要额外指示,用户能看到文字在增加
120
+ if (lastPart.type === 'text') {
121
+ return { type: 'none' }
122
+ }
123
+
124
+ // 工具调用完成后 → 显示规划提示
125
+ if (lastPart.type === 'tool_call') {
126
+ const status = (lastPart as ToolCallPart).status
127
+ if (status === 'done' || status === 'error' || status === 'skipped') {
128
+ return { type: 'text', text: '正在规划下一步...' }
129
+ }
130
+ // 工具正在执行时,卡片本身有状态,不需要额外指示
131
+ return { type: 'none' }
132
+ }
133
+
134
+ // 搜索完成后 → 显示规划提示
135
+ if (lastPart.type === 'search') {
136
+ if ((lastPart as SearchPart).status === 'done') {
137
+ return { type: 'text', text: '正在规划下一步...' }
138
+ }
139
+ return { type: 'none' }
140
+ }
141
+
142
+ // 思考完成后 → 显示规划提示
143
+ if (lastPart.type === 'thinking') {
144
+ if ((lastPart as ThinkingPart).status === 'done') {
145
+ return { type: 'text', text: '正在规划下一步...' }
146
+ }
147
+ return { type: 'none' }
148
+ }
149
+
150
+ return { type: 'none' }
151
+ }, [loading, parts])
152
+
153
+ return (
154
+ <div className={`message-bubble ${role}`}>
155
+ {/* 用户消息 */}
156
+ {isUser ? (
157
+ onSend && inputContext ? (
158
+ <ChatInput
159
+ variant="message"
160
+ value={userText}
161
+ mode={inputContext.mode}
162
+ model={inputContext.model}
163
+ models={inputContext.models}
164
+ webSearchEnabled={inputContext.webSearch}
165
+ thinkingEnabled={inputContext.thinking}
166
+ isLoading={inputContext.isLoading}
167
+ onSend={onSend}
168
+ onModeChange={inputContext.setMode}
169
+ onModelChange={inputContext.setModel}
170
+ onWebSearchChange={inputContext.setWebSearch}
171
+ onThinkingChange={inputContext.setThinking}
172
+ />
173
+ ) : (
174
+ <div className="user-content">
175
+ <div className="user-text">{userText}</div>
176
+ {images && images.length > 0 && (
177
+ <div className="user-images">
178
+ {images.map((img, i) => (
179
+ <img key={i} src={img} className="user-image" alt="" />
180
+ ))}
181
+ </div>
182
+ )}
183
+ {formattedTime && <div className="message-time user-time">{formattedTime}</div>}
184
+ </div>
185
+ )
186
+ ) : (
187
+ /* 助手消息 - 使用 PartsRenderer 渲染 */
188
+ <>
189
+ <div className="assistant-content">
190
+ <PartsRenderer
191
+ parts={parts}
192
+ expandedType={stepsExpandedType}
193
+ adapter={adapter}
194
+ autoRunConfig={autoRunConfig}
195
+ onSaveConfig={onSaveConfig}
196
+ />
197
+
198
+ {/* 加载指示器:等待状态时显示 */}
199
+ {loadingState.type === 'text' && (
200
+ <div className="loading-indicator">
201
+ <span className="loading-text">{loadingState.text}</span>
202
+ <span className="loading-shimmer"></span>
203
+ </div>
204
+ )}
205
+ </div>
206
+
207
+ {/* 操作按钮 */}
208
+ {hasContent && loading === false && (
209
+ <div className="message-actions">
210
+ {/* 左侧:模型信息 */}
211
+ <div className="message-meta">
212
+ {model && <span className="model-name">{getModelDisplayName(model, inputContext?.models)}</span>}
213
+ {mode && <span className="mode-badge">{mode === 'ask' ? 'Ask' : 'Agent'}</span>}
214
+ </div>
215
+ {/* 右侧:时间和按钮 */}
216
+ <div className="action-buttons">
217
+ {formattedTime && <span className="message-time assistant-time">{formattedTime}</span>}
218
+ <button className={`action-btn${copied ? ' copied' : ''}`} onClick={onCopy} title="复制">
219
+ <Icon icon={copied ? 'lucide:check' : 'lucide:copy'} width={14} />
220
+ </button>
221
+ <button className="action-btn" onClick={onRegenerate} title="重新生成">
222
+ <Icon icon="lucide:refresh-cw" width={14} />
223
+ </button>
224
+ </div>
225
+ </div>
226
+ )}
227
+ </>
228
+ )}
229
+ </div>
230
+ )
231
+ }
@@ -0,0 +1,4 @@
1
+ .parts-renderer {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
@@ -0,0 +1,114 @@
1
+ import type { FC } from 'react'
2
+ import type { ContentPart, StepsExpandedType } from '../../types'
3
+ import type { ChatAdapter } from '../../adapter'
4
+ import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
5
+ import {
6
+ TextPart,
7
+ ThinkingPart,
8
+ SearchPart,
9
+ ToolCallPart,
10
+ ToolResultPart,
11
+ ImagePart,
12
+ ErrorPart
13
+ } from './parts'
14
+ import './PartsRenderer.css'
15
+
16
+ interface PartsRendererProps {
17
+ parts: ContentPart[]
18
+ expandedType?: StepsExpandedType
19
+ // 工具调用相关
20
+ adapter?: ChatAdapter
21
+ autoRunConfig?: AutoRunConfig
22
+ onSaveConfig?: (config: AutoRunConfig) => Promise<void>
23
+ }
24
+
25
+ export const PartsRenderer: FC<PartsRendererProps> = ({
26
+ parts,
27
+ expandedType = 'auto',
28
+ adapter,
29
+ autoRunConfig,
30
+ onSaveConfig,
31
+ }) => {
32
+ return (
33
+ <div className="parts-renderer">
34
+ {parts.map((part, index) => {
35
+ switch (part.type) {
36
+ case 'text':
37
+ return <TextPart key={index} text={part.text} />
38
+
39
+ case 'thinking':
40
+ return (
41
+ <ThinkingPart
42
+ key={index}
43
+ text={part.text}
44
+ status={part.status}
45
+ duration={part.duration}
46
+ expandedType={expandedType}
47
+ />
48
+ )
49
+
50
+ case 'search':
51
+ return (
52
+ <SearchPart
53
+ key={index}
54
+ query={part.query}
55
+ results={part.results}
56
+ status={part.status}
57
+ expandedType={expandedType}
58
+ />
59
+ )
60
+
61
+ case 'tool_call':
62
+ return (
63
+ <ToolCallPart
64
+ key={index}
65
+ id={part.id}
66
+ name={part.name}
67
+ args={part.args}
68
+ status={part.status}
69
+ result={part.result}
70
+ expandedType={expandedType}
71
+ adapter={adapter}
72
+ autoRunConfig={autoRunConfig}
73
+ onSaveConfig={onSaveConfig}
74
+ />
75
+ )
76
+
77
+ case 'tool_result':
78
+ return (
79
+ <ToolResultPart
80
+ key={index}
81
+ id={part.id}
82
+ name={part.name}
83
+ args={part.args}
84
+ result={part.result}
85
+ status={part.status}
86
+ expandedType={expandedType}
87
+ />
88
+ )
89
+
90
+ case 'image':
91
+ return (
92
+ <ImagePart
93
+ key={index}
94
+ url={part.url}
95
+ />
96
+ )
97
+
98
+ case 'error':
99
+ return (
100
+ <ErrorPart
101
+ key={index}
102
+ message={part.message}
103
+ category={part.category}
104
+ retryable={part.retryable}
105
+ />
106
+ )
107
+
108
+ default:
109
+ return null
110
+ }
111
+ })}
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 工具结果渲染器
3
+ * 根据工具名称选择合适的渲染组件
4
+ */
5
+
6
+ import { FC, useContext, useMemo, type ComponentType } from 'react'
7
+ import type { ToolRendererProps } from '@huyooo/ai-chat-shared'
8
+ import { ToolRenderersContext } from '../../context/RenderersContext'
9
+ import { DefaultToolResult } from './tool-results'
10
+
11
+ export const ToolResultRenderer: FC<ToolRendererProps> = (props) => {
12
+ // 从上层获取自定义工具渲染器
13
+ const customRenderers = useContext(ToolRenderersContext)
14
+
15
+ // 解析使用哪个组件
16
+ const Component = useMemo<ComponentType<ToolRendererProps>>(() => {
17
+ return customRenderers[props.toolName] || DefaultToolResult
18
+ }, [customRenderers, props.toolName])
19
+
20
+ return <Component {...props} />
21
+ }
@@ -0,0 +1,221 @@
1
+ .welcome-message {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: 28px;
7
+ padding: 40px 24px;
8
+ max-width: 640px;
9
+ margin: 0 auto;
10
+ min-height: 100%;
11
+ }
12
+
13
+ /* 标题区域 */
14
+ .welcome-header {
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: center;
18
+ text-align: center;
19
+ gap: 8px;
20
+ }
21
+
22
+ .welcome-title-row {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 10px;
26
+ }
27
+
28
+ .welcome-icon {
29
+ color: var(--chat-text-muted, #888);
30
+ }
31
+
32
+ .welcome-title {
33
+ font-size: 24px;
34
+ font-weight: 600;
35
+ color: var(--chat-text, #fff);
36
+ margin: 0;
37
+ }
38
+
39
+ .welcome-subtitle {
40
+ font-size: 13px;
41
+ color: var(--chat-text-muted, #888);
42
+ margin: 0;
43
+ }
44
+
45
+ /* 区域标题 */
46
+ .section-header {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 6px;
50
+ margin-bottom: 12px;
51
+ padding-left: 2px;
52
+ }
53
+
54
+ .section-icon {
55
+ color: var(--chat-text-muted, #666);
56
+ }
57
+
58
+ .section-title {
59
+ font-size: 12px;
60
+ font-weight: 500;
61
+ color: var(--chat-text-muted, #888);
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.5px;
64
+ }
65
+
66
+ /* 能力标签 */
67
+ .features-section {
68
+ width: 100%;
69
+ }
70
+
71
+ .features-list {
72
+ display: flex;
73
+ flex-wrap: wrap;
74
+ gap: 8px;
75
+ justify-content: center;
76
+ }
77
+
78
+ .feature-tag {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 6px;
82
+ padding: 6px 12px;
83
+ background: var(--chat-muted, #2a2a2a);
84
+ border: 1px solid var(--chat-border, #3a3a3a);
85
+ border-radius: 20px;
86
+ font-size: 12px;
87
+ color: var(--chat-text, #ccc);
88
+ transition: all 0.15s;
89
+ }
90
+
91
+ .feature-tag:hover {
92
+ background: rgba(255, 255, 255, 0.08);
93
+ border-color: rgba(255, 255, 255, 0.15);
94
+ }
95
+
96
+ .feature-icon {
97
+ color: var(--chat-text-muted, #888);
98
+ }
99
+
100
+ /* 快捷操作 */
101
+ .tasks-section {
102
+ width: 100%;
103
+ }
104
+
105
+ .tasks-grid {
106
+ display: grid;
107
+ gap: 10px;
108
+ }
109
+
110
+ /* 单个任务:居中显示 */
111
+ .tasks-single {
112
+ grid-template-columns: minmax(200px, 320px);
113
+ justify-content: center;
114
+ }
115
+
116
+ /* 两个任务:两列 */
117
+ .tasks-two {
118
+ grid-template-columns: repeat(2, 1fr);
119
+ }
120
+
121
+ /* 三个任务:三列 */
122
+ .tasks-three {
123
+ grid-template-columns: repeat(3, 1fr);
124
+ }
125
+
126
+ /* 多个任务:两列自动换行 */
127
+ .tasks-multi {
128
+ grid-template-columns: repeat(2, 1fr);
129
+ }
130
+
131
+ .task-card {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 10px;
135
+ padding: 12px 14px;
136
+ background: var(--chat-muted, #2a2a2a);
137
+ border: 1px solid var(--chat-border, #3a3a3a);
138
+ border-radius: 10px;
139
+ color: var(--chat-text, #ccc);
140
+ text-align: left;
141
+ cursor: pointer;
142
+ transition: all 0.2s;
143
+ }
144
+
145
+ .task-card:hover {
146
+ background: rgba(255, 255, 255, 0.08);
147
+ border-color: rgba(59, 130, 246, 0.5);
148
+ transform: translateY(-1px);
149
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
150
+ }
151
+
152
+ .task-icon {
153
+ flex-shrink: 0;
154
+ width: 32px;
155
+ height: 32px;
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ background: rgba(59, 130, 246, 0.15);
160
+ border-radius: 8px;
161
+ color: #60a5fa;
162
+ }
163
+
164
+ .task-card:hover .task-icon {
165
+ background: rgba(59, 130, 246, 0.25);
166
+ }
167
+
168
+ .task-content {
169
+ flex: 1;
170
+ min-width: 0;
171
+ }
172
+
173
+ .task-name {
174
+ font-size: 13px;
175
+ font-weight: 500;
176
+ color: var(--chat-text, #fff);
177
+ margin-bottom: 2px;
178
+ }
179
+
180
+ .task-desc {
181
+ font-size: 11px;
182
+ color: var(--chat-text-muted, #888);
183
+ white-space: nowrap;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ }
187
+
188
+ .task-arrow {
189
+ flex-shrink: 0;
190
+ color: var(--chat-text-muted, #666);
191
+ opacity: 0;
192
+ transform: translateX(-4px);
193
+ transition: all 0.2s;
194
+ }
195
+
196
+ .task-card:hover .task-arrow {
197
+ opacity: 1;
198
+ transform: translateX(0);
199
+ }
200
+
201
+ /* 响应式 */
202
+ @media (max-width: 540px) {
203
+ .welcome-message {
204
+ padding: 32px 16px;
205
+ gap: 24px;
206
+ }
207
+
208
+ .welcome-title {
209
+ font-size: 24px;
210
+ }
211
+
212
+ .tasks-two,
213
+ .tasks-three,
214
+ .tasks-multi {
215
+ grid-template-columns: 1fr;
216
+ }
217
+
218
+ .task-arrow {
219
+ display: none;
220
+ }
221
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * WelcomeMessage Component
3
+ * 与 Vue 版本 WelcomeMessage.vue 保持一致
4
+ */
5
+
6
+ import { type FC, useMemo } from 'react'
7
+ import './WelcomeMessage.css'
8
+ import { Icon } from '@iconify/react'
9
+ import { type WelcomeConfig, defaultWelcomeConfig } from './welcome-types'
10
+
11
+ interface WelcomeMessageProps {
12
+ /** 欢迎页配置(可部分配置,未配置项使用默认值) */
13
+ config?: Partial<WelcomeConfig>
14
+ /** 快捷操作回调 */
15
+ onQuickAction: (text: string) => void
16
+ }
17
+
18
+ export const WelcomeMessage: FC<WelcomeMessageProps> = ({ config: propsConfig, onQuickAction }) => {
19
+ // 合并配置
20
+ const config = useMemo<WelcomeConfig>(() => ({
21
+ title: propsConfig?.title ?? defaultWelcomeConfig.title,
22
+ subtitle: propsConfig?.subtitle ?? defaultWelcomeConfig.subtitle,
23
+ icon: propsConfig?.icon ?? defaultWelcomeConfig.icon,
24
+ features: propsConfig?.features ?? defaultWelcomeConfig.features,
25
+ tasks: propsConfig?.tasks ?? defaultWelcomeConfig.tasks,
26
+ }), [propsConfig])
27
+
28
+ // 根据任务数量动态选择网格布局
29
+ const tasksGridClass = useMemo(() => {
30
+ const count = config.tasks.length
31
+ if (count === 1) return 'tasks-grid tasks-single'
32
+ if (count === 2) return 'tasks-grid tasks-two'
33
+ if (count === 3) return 'tasks-grid tasks-three'
34
+ return 'tasks-grid tasks-multi'
35
+ }, [config.tasks.length])
36
+
37
+ return (
38
+ <div className="welcome-message">
39
+ {/* 标题区域 */}
40
+ <div className="welcome-header">
41
+ <div className="welcome-title-row">
42
+ <Icon icon={config.icon} width={28} className="welcome-icon" />
43
+ <h2 className="welcome-title">{config.title}</h2>
44
+ </div>
45
+ <p className="welcome-subtitle">{config.subtitle}</p>
46
+ </div>
47
+
48
+ {/* 能力标签 */}
49
+ {config.features.length > 0 && (
50
+ <div className="features-section">
51
+ <div className="section-header">
52
+ <Icon icon="lucide:zap" width={14} className="section-icon" />
53
+ <span className="section-title">支持的能力</span>
54
+ </div>
55
+ <div className="features-list">
56
+ {config.features.map((feature) => (
57
+ <div key={feature.name} className="feature-tag">
58
+ <Icon icon={feature.icon} width={14} className="feature-icon" />
59
+ <span>{feature.name}</span>
60
+ </div>
61
+ ))}
62
+ </div>
63
+ </div>
64
+ )}
65
+
66
+ {/* 快捷操作 */}
67
+ {config.tasks.length > 0 && (
68
+ <div className="tasks-section">
69
+ <div className="section-header">
70
+ <Icon icon="lucide:rocket" width={14} className="section-icon" />
71
+ <span className="section-title">快捷操作</span>
72
+ </div>
73
+ <div className={tasksGridClass}>
74
+ {config.tasks.map((task) => (
75
+ <button
76
+ key={task.name}
77
+ className="task-card"
78
+ onClick={() => onQuickAction(task.prompt)}
79
+ >
80
+ <Icon icon={task.icon} width={18} className="task-icon" />
81
+ <div className="task-content">
82
+ <div className="task-name">{task.name}</div>
83
+ <div className="task-desc">{task.desc}</div>
84
+ </div>
85
+ <Icon icon="lucide:arrow-right" width={14} className="task-arrow" />
86
+ </button>
87
+ ))}
88
+ </div>
89
+ </div>
90
+ )}
91
+ </div>
92
+ )
93
+ }