@huyooo/ai-chat-frontend-react 0.2.14 → 0.2.16

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 (76) hide show
  1. package/dist/index.css +0 -1
  2. package/dist/index.js +1 -5418
  3. package/package.json +4 -5
  4. package/dist/index.css.map +0 -1
  5. package/dist/index.js.map +0 -1
  6. package/src/adapter.ts +0 -68
  7. package/src/components/ChatPanel.tsx +0 -553
  8. package/src/components/common/ConfirmDialog.css +0 -136
  9. package/src/components/common/ConfirmDialog.tsx +0 -91
  10. package/src/components/common/CopyButton.css +0 -22
  11. package/src/components/common/CopyButton.tsx +0 -46
  12. package/src/components/common/IndexingSettings.css +0 -207
  13. package/src/components/common/IndexingSettings.tsx +0 -398
  14. package/src/components/common/SettingsPanel.css +0 -337
  15. package/src/components/common/SettingsPanel.tsx +0 -215
  16. package/src/components/common/Toast.css +0 -50
  17. package/src/components/common/Toast.tsx +0 -38
  18. package/src/components/common/ToggleSwitch.css +0 -52
  19. package/src/components/common/ToggleSwitch.tsx +0 -20
  20. package/src/components/header/ChatHeader.css +0 -285
  21. package/src/components/header/ChatHeader.tsx +0 -376
  22. package/src/components/input/AtFilePicker.css +0 -147
  23. package/src/components/input/AtFilePicker.tsx +0 -519
  24. package/src/components/input/ChatInput.css +0 -283
  25. package/src/components/input/ChatInput.tsx +0 -575
  26. package/src/components/input/DropdownSelector.css +0 -231
  27. package/src/components/input/DropdownSelector.tsx +0 -333
  28. package/src/components/input/ImagePreviewModal.css +0 -124
  29. package/src/components/input/ImagePreviewModal.tsx +0 -118
  30. package/src/components/input/at-views/AtBranchView.tsx +0 -34
  31. package/src/components/input/at-views/AtBrowserView.tsx +0 -34
  32. package/src/components/input/at-views/AtChatsView.tsx +0 -34
  33. package/src/components/input/at-views/AtDocsView.tsx +0 -34
  34. package/src/components/input/at-views/AtFilesView.tsx +0 -168
  35. package/src/components/input/at-views/AtTerminalsView.tsx +0 -34
  36. package/src/components/input/at-views/AtViewStyles.css +0 -143
  37. package/src/components/input/at-views/index.ts +0 -9
  38. package/src/components/message/ContentRenderer.css +0 -9
  39. package/src/components/message/MessageBubble.css +0 -193
  40. package/src/components/message/MessageBubble.tsx +0 -240
  41. package/src/components/message/PartsRenderer.css +0 -12
  42. package/src/components/message/PartsRenderer.tsx +0 -168
  43. package/src/components/message/WelcomeMessage.css +0 -221
  44. package/src/components/message/WelcomeMessage.tsx +0 -93
  45. package/src/components/message/parts/CollapsibleCard.css +0 -80
  46. package/src/components/message/parts/CollapsibleCard.tsx +0 -80
  47. package/src/components/message/parts/ErrorPart.css +0 -9
  48. package/src/components/message/parts/ErrorPart.tsx +0 -40
  49. package/src/components/message/parts/ImagePart.css +0 -49
  50. package/src/components/message/parts/ImagePart.tsx +0 -54
  51. package/src/components/message/parts/SearchPart.css +0 -44
  52. package/src/components/message/parts/SearchPart.tsx +0 -63
  53. package/src/components/message/parts/TextPart.css +0 -579
  54. package/src/components/message/parts/TextPart.tsx +0 -213
  55. package/src/components/message/parts/ThinkingPart.css +0 -9
  56. package/src/components/message/parts/ThinkingPart.tsx +0 -48
  57. package/src/components/message/parts/ToolCallPart.css +0 -246
  58. package/src/components/message/parts/ToolCallPart.tsx +0 -289
  59. package/src/components/message/parts/ToolResultPart.css +0 -67
  60. package/src/components/message/parts/index.ts +0 -13
  61. package/src/components/message/parts/visual-predicate.ts +0 -43
  62. package/src/components/message/parts/visual-render.ts +0 -19
  63. package/src/components/message/parts/visual.ts +0 -12
  64. package/src/components/message/welcome-types.ts +0 -46
  65. package/src/context/AutoRunConfigContext.tsx +0 -13
  66. package/src/context/ChatAdapterContext.tsx +0 -8
  67. package/src/context/ChatInputContext.tsx +0 -40
  68. package/src/context/RenderersContext.tsx +0 -35
  69. package/src/hooks/useChat.ts +0 -1569
  70. package/src/hooks/useImageUpload.ts +0 -345
  71. package/src/hooks/useVoiceInput.ts +0 -454
  72. package/src/hooks/useVoiceToTextInput.ts +0 -87
  73. package/src/index.ts +0 -151
  74. package/src/styles.css +0 -330
  75. package/src/types/index.ts +0 -196
  76. package/src/utils/fileIcon.ts +0 -49
@@ -1,193 +0,0 @@
1
- /**
2
- * MessageBubble 组件样式
3
- * 与 Vue 版本 MessageBubble.vue 保持一致
4
- */
5
-
6
- .message-bubble {
7
- animation: fadeIn 0.2s ease;
8
- }
9
-
10
- @keyframes fadeIn {
11
- from {
12
- opacity: 0;
13
- transform: translateY(4px);
14
- }
15
- to {
16
- opacity: 1;
17
- transform: translateY(0);
18
- }
19
- }
20
-
21
- /* 用户消息 */
22
- .message-bubble.user {
23
- width: 100%;
24
- }
25
-
26
- .user-content {
27
- width: 100%;
28
- background: var(--chat-muted, #2d2d2d);
29
- color: var(--chat-text, #ccc);
30
- padding: 12px;
31
- border-radius: 12px;
32
- border: 1px solid var(--chat-border, #444);
33
- }
34
-
35
- .user-text {
36
- font-size: 14px;
37
- line-height: 1.5;
38
- white-space: pre-wrap;
39
- word-break: break-word;
40
- }
41
-
42
- .user-images {
43
- display: flex;
44
- gap: 8px;
45
- margin-top: 8px;
46
- flex-wrap: wrap;
47
- }
48
-
49
- .user-image {
50
- width: 80px;
51
- height: 80px;
52
- object-fit: cover;
53
- border-radius: 8px;
54
- cursor: pointer;
55
- transition: transform 0.15s;
56
- }
57
-
58
- .user-image:hover {
59
- transform: scale(1.05);
60
- }
61
-
62
- /* 助手消息 */
63
- .message-bubble.assistant {
64
- position: relative;
65
- }
66
-
67
- .assistant-content {
68
- max-width: 100%;
69
- }
70
-
71
- /* 加载提示 - 等待状态时 */
72
- .loading-indicator {
73
- position: relative;
74
- display: flex;
75
- align-items: center;
76
- width: 100%;
77
- padding: 10px 16px;
78
- background: var(--chat-muted, #2a2a2a);
79
- border-radius: 8px;
80
- overflow: hidden;
81
- }
82
-
83
- /* 当上方有内容时,添加间距 */
84
- .loading-indicator.has-content-above {
85
- margin-top: 8px;
86
- }
87
-
88
- .loading-text {
89
- font-size: 13px;
90
- color: var(--chat-text-muted, #888);
91
- position: relative;
92
- z-index: 1;
93
- }
94
-
95
- .loading-shimmer {
96
- position: absolute;
97
- top: 0;
98
- left: -100%;
99
- width: 100%;
100
- height: 100%;
101
- background: linear-gradient(
102
- 90deg,
103
- transparent 0%,
104
- rgba(255, 255, 255, 0.08) 50%,
105
- transparent 100%
106
- );
107
- animation: shimmer 1.5s ease-in-out infinite;
108
- }
109
-
110
- @keyframes shimmer {
111
- 0% {
112
- left: -100%;
113
- }
114
- 100% {
115
- left: 100%;
116
- }
117
- }
118
-
119
- /* 操作按钮 */
120
- .message-actions {
121
- display: flex;
122
- align-items: center;
123
- justify-content: space-between;
124
- margin-top: 8px;
125
- }
126
-
127
- .message-meta {
128
- display: flex;
129
- align-items: center;
130
- gap: 6px;
131
- }
132
-
133
- .model-name {
134
- font-size: 11px;
135
- color: var(--chat-text-muted, #888);
136
- background: var(--chat-muted, #2a2a2a);
137
- padding: 2px 8px;
138
- border-radius: 10px;
139
- border: 1px solid var(--chat-border, #333);
140
- }
141
-
142
- .mode-badge {
143
- font-size: 10px;
144
- color: var(--chat-text-muted, #888);
145
- background: var(--chat-muted, #2a2a2a);
146
- padding: 2px 6px;
147
- border-radius: 10px;
148
- border: 1px solid var(--chat-border, #333);
149
- }
150
-
151
- .action-buttons {
152
- display: flex;
153
- align-items: center;
154
- gap: 4px;
155
- }
156
-
157
- .action-btn {
158
- display: flex;
159
- align-items: center;
160
- justify-content: center;
161
- width: 24px;
162
- height: 24px;
163
- border: none;
164
- background: transparent;
165
- border-radius: 4px;
166
- color: var(--chat-text-muted, #666);
167
- cursor: pointer;
168
- transition: all 0.15s;
169
- }
170
-
171
- .action-btn:hover {
172
- background: var(--chat-muted, #3c3c3c);
173
- color: var(--chat-text, #ccc);
174
- }
175
-
176
- .action-btn.copied {
177
- color: #22c55e;
178
- }
179
-
180
- /* 消息时间 */
181
- .message-time {
182
- font-size: 12px;
183
- color: var(--chat-text-muted, #666);
184
- }
185
-
186
- .user-time {
187
- text-align: right;
188
- margin-top: 8px;
189
- }
190
-
191
- .assistant-time {
192
- margin-right: 8px;
193
- }
@@ -1,240 +0,0 @@
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
- /** 取消工具调用(通常会中止当前请求/流式输出) */
41
- onCancelToolCall?: (toolCallId: string) => void
42
- autoRunConfig?: AutoRunConfig
43
- onSaveConfig?: (config: AutoRunConfig) => Promise<void>
44
- }
45
-
46
- /** 获取模型显示名称 */
47
- function getModelDisplayName(modelId: string, models?: ModelOption[]): string {
48
- // 优先从传入的 models 中查找
49
- const found = models?.find((m) => m.modelId === modelId)
50
- if (found) return found.displayName
51
- // 后备:提取模型名称(前端不应该依赖后端包)
52
- return modelId.split('/').pop() || modelId
53
- }
54
-
55
- /** 格式化时间显示 */
56
- function formatTime(timestamp?: Date | string | number): string {
57
- if (!timestamp) return ''
58
- const date = new Date(timestamp)
59
- const year = date.getFullYear()
60
- const month = String(date.getMonth() + 1).padStart(2, '0')
61
- const day = String(date.getDate()).padStart(2, '0')
62
- const hours = String(date.getHours()).padStart(2, '0')
63
- const minutes = String(date.getMinutes()).padStart(2, '0')
64
- return `${year}-${month}-${day} ${hours}:${minutes}`
65
- }
66
-
67
- export const MessageBubble: FC<MessageBubbleProps> = ({
68
- role,
69
- parts,
70
- model,
71
- mode,
72
- images,
73
- loading,
74
- copied,
75
- timestamp,
76
- onCopy,
77
- onRegenerate,
78
- onSend,
79
- stepsExpandedType = 'auto',
80
- adapter,
81
- onCancelToolCall,
82
- autoRunConfig,
83
- onSaveConfig,
84
- }) => {
85
- const isUser = role === 'user'
86
- const formattedTime = formatTime(timestamp)
87
- const inputContext = useChatInputContext()
88
-
89
- // 提取用户消息的文本内容
90
- const userText = useMemo(() => {
91
- return parts
92
- .filter((p): p is TextPart => p.type === 'text')
93
- .map(p => p.text)
94
- .join('')
95
- }, [parts])
96
-
97
- // 是否有内容(用于显示操作按钮)
98
- const hasContent = useMemo(() => {
99
- return parts.some(p =>
100
- (p.type === 'text' && (p as TextPart).text) ||
101
- p.type === 'thinking' ||
102
- p.type === 'search' ||
103
- p.type === 'tool_call'
104
- )
105
- }, [parts])
106
-
107
- // loading 状态:决定显示什么类型的指示器
108
- // text: 文字提示(等待状态时)
109
- // none: 不显示(有正在进行的活动)
110
- const loadingState = useMemo<{ type: 'text' | 'none'; text?: string }>(() => {
111
- if (!loading) {
112
- return { type: 'none' }
113
- }
114
-
115
- // 分模式显示:ask 模式直接生成回答,agent 模式规划下一步
116
- const waitingText = mode === 'ask' ? '正在生成回答...' : '正在规划下一步...'
117
-
118
- // 没有任何 parts 时,显示等待
119
- if (parts.length === 0) {
120
- return { type: 'text', text: waitingText }
121
- }
122
-
123
- // 检查是否有正在进行的活动(如果有,不需要显示等待提示)
124
- const hasActiveActivity = parts.some(part => {
125
- // 思考正在进行
126
- if (part.type === 'thinking' && (part as ThinkingPart).status === 'running') {
127
- return true
128
- }
129
- // 搜索正在进行
130
- if (part.type === 'search' && (part as SearchPart).status === 'running') {
131
- return true
132
- }
133
- // 工具调用正在进行(running 或 pending)
134
- if (part.type === 'tool_call') {
135
- const status = (part as ToolCallPart).status
136
- if (status === 'running' || status === 'pending') {
137
- return true
138
- }
139
- }
140
- return false
141
- })
142
-
143
- // 有正在进行的活动 → 不显示等待提示(活动本身有状态显示)
144
- if (hasActiveActivity) {
145
- return { type: 'none' }
146
- }
147
-
148
- // 检查最后一个 part 是否是正在流式输出的文本
149
- const lastPart = parts[parts.length - 1]
150
- if (lastPart.type === 'text') {
151
- // 文本正在流式输出 → 不显示等待提示
152
- return { type: 'none' }
153
- }
154
-
155
- // 没有任何正在进行的活动,但 loading=true → 显示等待提示
156
- // 这包括:工具执行完成后、思考完成后、搜索完成后等"空窗期"
157
- return { type: 'text', text: waitingText }
158
- }, [loading, parts, mode])
159
-
160
- return (
161
- <div className={`message-bubble ${role}`}>
162
- {/* 用户消息 */}
163
- {isUser ? (
164
- onSend && inputContext ? (
165
- <ChatInput
166
- variant="message"
167
- value={userText}
168
- images={images}
169
- mode={inputContext.mode}
170
- model={inputContext.model}
171
- models={inputContext.models}
172
- webSearchEnabled={inputContext.webSearch}
173
- thinkingEnabled={inputContext.thinking}
174
- isLoading={inputContext.isLoading}
175
- onSend={onSend}
176
- onModeChange={inputContext.setMode}
177
- onModelChange={inputContext.setModel}
178
- onWebSearchChange={inputContext.setWebSearch}
179
- onThinkingChange={inputContext.setThinking}
180
- />
181
- ) : (
182
- <div className="user-content">
183
- <div className="user-text">{userText}</div>
184
- {images && images.length > 0 && (
185
- <div className="user-images">
186
- {images.map((img, i) => (
187
- <img key={i} src={img} className="user-image" alt="" />
188
- ))}
189
- </div>
190
- )}
191
- {formattedTime && <div className="message-time user-time">{formattedTime}</div>}
192
- </div>
193
- )
194
- ) : (
195
- /* 助手消息 - 使用 PartsRenderer 渲染 */
196
- <>
197
- <div className="assistant-content">
198
- <PartsRenderer
199
- parts={parts}
200
- expandedType={stepsExpandedType}
201
- adapter={adapter}
202
- onCancelToolCall={onCancelToolCall}
203
- autoRunConfig={autoRunConfig}
204
- onSaveConfig={onSaveConfig}
205
- />
206
-
207
- {/* 加载指示器:等待状态时显示 */}
208
- {loadingState.type === 'text' && (
209
- <div className={`loading-indicator${parts.length > 0 ? ' has-content-above' : ''}`}>
210
- <span className="loading-text">{loadingState.text}</span>
211
- <span className="loading-shimmer"></span>
212
- </div>
213
- )}
214
- </div>
215
-
216
- {/* 操作按钮 */}
217
- {hasContent && loading === false && (
218
- <div className="message-actions">
219
- {/* 左侧:模型信息 */}
220
- <div className="message-meta">
221
- {model && <span className="model-name">{getModelDisplayName(model, inputContext?.models)}</span>}
222
- {mode && <span className="mode-badge">{mode === 'ask' ? 'Ask' : 'Agent'}</span>}
223
- </div>
224
- {/* 右侧:时间和按钮 */}
225
- <div className="action-buttons">
226
- {formattedTime && <span className="message-time assistant-time">{formattedTime}</span>}
227
- <button className={`action-btn${copied ? ' copied' : ''}`} onClick={onCopy} title="复制">
228
- <Icon icon={copied ? 'lucide:check' : 'lucide:copy'} width={14} />
229
- </button>
230
- <button className="action-btn" onClick={onRegenerate} title="重新生成">
231
- <Icon icon="lucide:refresh-cw" width={14} />
232
- </button>
233
- </div>
234
- </div>
235
- )}
236
- </>
237
- )}
238
- </div>
239
- )
240
- }
@@ -1,12 +0,0 @@
1
- .parts-renderer {
2
- display: flex;
3
- flex-direction: column;
4
- }
5
-
6
- .part-item {
7
- margin-top: 8px;
8
- }
9
-
10
- .part-item:first-child {
11
- margin-top: 0;
12
- }
@@ -1,168 +0,0 @@
1
- import { useContext, useMemo, type FC, type ComponentType } from 'react'
2
- import type {
3
- ContentPart,
4
- StepsExpandedType,
5
- TextPart as TextPartType,
6
- ThinkingPart as ThinkingPartType,
7
- SearchPart as SearchPartType,
8
- ToolCallPart as ToolCallPartType,
9
- ImagePart as ImagePartType,
10
- ErrorPart as ErrorPartType,
11
- } from '../../types'
12
- import type { ChatAdapter } from '../../adapter'
13
- import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
14
- import { PartRenderersContext } from '../../context/RenderersContext'
15
- import {
16
- TextPart,
17
- ThinkingPart,
18
- SearchPart,
19
- ImagePart,
20
- ErrorPart,
21
- } from './parts'
22
- import { ToolCallPart } from './parts/ToolCallPart'
23
- import './PartsRenderer.css'
24
-
25
- /** Part 渲染器映射类型 */
26
- type PartRenderers = Record<string, ComponentType<Record<string, unknown>>>
27
-
28
- interface PartsRendererProps {
29
- parts: ContentPart[]
30
- expandedType?: StepsExpandedType
31
- /** 自定义 Part 渲染器(props 传入优先级高于 context) */
32
- partRenderers?: PartRenderers
33
- // 工具调用相关
34
- adapter?: ChatAdapter
35
- onCancelToolCall?: (toolCallId: string) => void
36
- autoRunConfig?: AutoRunConfig
37
- onSaveConfig?: (config: AutoRunConfig) => Promise<void>
38
- }
39
-
40
- // ==================== 类型守卫函数 ====================
41
-
42
- function isText(part: ContentPart): part is TextPartType {
43
- return part.type === 'text'
44
- }
45
-
46
- function isThinking(part: ContentPart): part is ThinkingPartType {
47
- return part.type === 'thinking'
48
- }
49
-
50
- function isSearch(part: ContentPart): part is SearchPartType {
51
- return part.type === 'search'
52
- }
53
-
54
- function isToolCall(part: ContentPart): part is ToolCallPartType {
55
- return part.type === 'tool_call'
56
- }
57
-
58
- function isImage(part: ContentPart): part is ImagePartType {
59
- return part.type === 'image'
60
- }
61
-
62
- function isError(part: ContentPart): part is ErrorPartType {
63
- return part.type === 'error'
64
- }
65
-
66
- export const PartsRenderer: FC<PartsRendererProps> = ({
67
- parts,
68
- expandedType = 'auto',
69
- partRenderers: propRenderers,
70
- adapter,
71
- onCancelToolCall,
72
- autoRunConfig,
73
- onSaveConfig,
74
- }) => {
75
- // 从 context 获取渲染器,props 优先
76
- const contextRenderers = useContext(PartRenderersContext)
77
- const partRenderers = propRenderers ?? contextRenderers
78
-
79
- /** 可见的 parts */
80
- const visibleParts = useMemo(() => parts, [parts])
81
-
82
- const renderPart = (part: ContentPart) => {
83
- // 先检查是否有自定义渲染器
84
- const CustomRenderer = partRenderers[part.type]
85
- if (CustomRenderer) {
86
- return <CustomRenderer {...part} />
87
- }
88
-
89
- // 内置类型渲染(使用类型守卫)
90
- if (isText(part)) {
91
- return <TextPart text={part.text} />
92
- }
93
-
94
- if (isThinking(part)) {
95
- return (
96
- <ThinkingPart
97
- text={part.text}
98
- status={part.status}
99
- duration={part.duration}
100
- expandedType={expandedType}
101
- />
102
- )
103
- }
104
-
105
- if (isSearch(part)) {
106
- return (
107
- <SearchPart
108
- query={part.query}
109
- results={part.results}
110
- status={part.status}
111
- expandedType={expandedType}
112
- />
113
- )
114
- }
115
-
116
- if (isToolCall(part)) {
117
- return (
118
- <ToolCallPart
119
- id={part.id}
120
- name={part.name}
121
- args={part.args}
122
- output={part.output}
123
- status={part.status}
124
- expandedType={expandedType}
125
- adapter={adapter}
126
- onCancelToolCall={onCancelToolCall}
127
- autoRunConfig={autoRunConfig}
128
- onSaveConfig={onSaveConfig}
129
- />
130
- )
131
- }
132
-
133
- if (isImage(part)) {
134
- return <ImagePart url={part.url} />
135
- }
136
-
137
- if (isError(part)) {
138
- return (
139
- <ErrorPart
140
- message={part.message}
141
- category={part.category}
142
- retryable={part.retryable}
143
- />
144
- )
145
- }
146
-
147
- // 未知类型,显示 JSON
148
- return (
149
- <div className="unknown-part">
150
- <pre>{JSON.stringify(part, null, 2)}</pre>
151
- </div>
152
- )
153
- }
154
-
155
- return (
156
- <div className="parts-renderer">
157
- {visibleParts.map((part, index) => (
158
- <div
159
- key={index}
160
- className="part-item"
161
- style={{ marginTop: index > 0 ? '8px' : '0' }}
162
- >
163
- {renderPart(part)}
164
- </div>
165
- ))}
166
- </div>
167
- )
168
- }