@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.
- package/dist/index.css +0 -1
- package/dist/index.js +1 -5418
- package/package.json +4 -5
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/src/adapter.ts +0 -68
- package/src/components/ChatPanel.tsx +0 -553
- package/src/components/common/ConfirmDialog.css +0 -136
- package/src/components/common/ConfirmDialog.tsx +0 -91
- package/src/components/common/CopyButton.css +0 -22
- package/src/components/common/CopyButton.tsx +0 -46
- package/src/components/common/IndexingSettings.css +0 -207
- package/src/components/common/IndexingSettings.tsx +0 -398
- package/src/components/common/SettingsPanel.css +0 -337
- package/src/components/common/SettingsPanel.tsx +0 -215
- package/src/components/common/Toast.css +0 -50
- package/src/components/common/Toast.tsx +0 -38
- package/src/components/common/ToggleSwitch.css +0 -52
- package/src/components/common/ToggleSwitch.tsx +0 -20
- package/src/components/header/ChatHeader.css +0 -285
- package/src/components/header/ChatHeader.tsx +0 -376
- package/src/components/input/AtFilePicker.css +0 -147
- package/src/components/input/AtFilePicker.tsx +0 -519
- package/src/components/input/ChatInput.css +0 -283
- package/src/components/input/ChatInput.tsx +0 -575
- package/src/components/input/DropdownSelector.css +0 -231
- package/src/components/input/DropdownSelector.tsx +0 -333
- package/src/components/input/ImagePreviewModal.css +0 -124
- package/src/components/input/ImagePreviewModal.tsx +0 -118
- package/src/components/input/at-views/AtBranchView.tsx +0 -34
- package/src/components/input/at-views/AtBrowserView.tsx +0 -34
- package/src/components/input/at-views/AtChatsView.tsx +0 -34
- package/src/components/input/at-views/AtDocsView.tsx +0 -34
- package/src/components/input/at-views/AtFilesView.tsx +0 -168
- package/src/components/input/at-views/AtTerminalsView.tsx +0 -34
- package/src/components/input/at-views/AtViewStyles.css +0 -143
- package/src/components/input/at-views/index.ts +0 -9
- package/src/components/message/ContentRenderer.css +0 -9
- package/src/components/message/MessageBubble.css +0 -193
- package/src/components/message/MessageBubble.tsx +0 -240
- package/src/components/message/PartsRenderer.css +0 -12
- package/src/components/message/PartsRenderer.tsx +0 -168
- package/src/components/message/WelcomeMessage.css +0 -221
- package/src/components/message/WelcomeMessage.tsx +0 -93
- package/src/components/message/parts/CollapsibleCard.css +0 -80
- package/src/components/message/parts/CollapsibleCard.tsx +0 -80
- package/src/components/message/parts/ErrorPart.css +0 -9
- package/src/components/message/parts/ErrorPart.tsx +0 -40
- package/src/components/message/parts/ImagePart.css +0 -49
- package/src/components/message/parts/ImagePart.tsx +0 -54
- package/src/components/message/parts/SearchPart.css +0 -44
- package/src/components/message/parts/SearchPart.tsx +0 -63
- package/src/components/message/parts/TextPart.css +0 -579
- package/src/components/message/parts/TextPart.tsx +0 -213
- package/src/components/message/parts/ThinkingPart.css +0 -9
- package/src/components/message/parts/ThinkingPart.tsx +0 -48
- package/src/components/message/parts/ToolCallPart.css +0 -246
- package/src/components/message/parts/ToolCallPart.tsx +0 -289
- package/src/components/message/parts/ToolResultPart.css +0 -67
- package/src/components/message/parts/index.ts +0 -13
- package/src/components/message/parts/visual-predicate.ts +0 -43
- package/src/components/message/parts/visual-render.ts +0 -19
- package/src/components/message/parts/visual.ts +0 -12
- package/src/components/message/welcome-types.ts +0 -46
- package/src/context/AutoRunConfigContext.tsx +0 -13
- package/src/context/ChatAdapterContext.tsx +0 -8
- package/src/context/ChatInputContext.tsx +0 -40
- package/src/context/RenderersContext.tsx +0 -35
- package/src/hooks/useChat.ts +0 -1569
- package/src/hooks/useImageUpload.ts +0 -345
- package/src/hooks/useVoiceInput.ts +0 -454
- package/src/hooks/useVoiceToTextInput.ts +0 -87
- package/src/index.ts +0 -151
- package/src/styles.css +0 -330
- package/src/types/index.ts +0 -196
- package/src/utils/fileIcon.ts +0 -49
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 图片上传 hook
|
|
3
|
-
* 处理图片选择、粘贴、拖拽、预览等功能
|
|
4
|
-
* 与 Vue 版本 useImageUpload.ts 保持一致
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useState, useCallback, useMemo } from 'react';
|
|
8
|
-
|
|
9
|
-
/** 内部图片数据结构(包含预览用的 dataUrl) */
|
|
10
|
-
export interface ImageItem {
|
|
11
|
-
dataUrl: string;
|
|
12
|
-
base64: string;
|
|
13
|
-
mimeType: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** 导出给外部使用的图片数据格式 */
|
|
17
|
-
export interface ImageData {
|
|
18
|
-
base64: string;
|
|
19
|
-
mimeType: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** useImageUpload 配置项 */
|
|
23
|
-
interface UseImageUploadOptions {
|
|
24
|
-
/** 最大图片数量,默认 5 */
|
|
25
|
-
maxImages?: number;
|
|
26
|
-
/** 单张图片最大大小(字节),默认 10MB */
|
|
27
|
-
maxSize?: number;
|
|
28
|
-
/** 图片最大宽度,默认 4000px(避免超过 API 像素限制) */
|
|
29
|
-
maxWidth?: number;
|
|
30
|
-
/** 图片最大高度,默认 4000px(避免超过 API 像素限制) */
|
|
31
|
-
maxHeight?: number;
|
|
32
|
-
/** 压缩质量 0-1,默认 0.85 */
|
|
33
|
-
quality?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 图片上传 hook
|
|
38
|
-
* @param options 配置项
|
|
39
|
-
*/
|
|
40
|
-
export function useImageUpload(options: UseImageUploadOptions = {}) {
|
|
41
|
-
const {
|
|
42
|
-
maxImages = 5,
|
|
43
|
-
maxSize = 10 * 1024 * 1024,
|
|
44
|
-
maxWidth = 4000,
|
|
45
|
-
maxHeight = 4000,
|
|
46
|
-
quality = 0.85
|
|
47
|
-
} = options;
|
|
48
|
-
|
|
49
|
-
// 图片列表
|
|
50
|
-
const [images, setImages] = useState<ImageItem[]>([]);
|
|
51
|
-
|
|
52
|
-
// 拖拽状态
|
|
53
|
-
const [isDragOver, setIsDragOver] = useState(false);
|
|
54
|
-
|
|
55
|
-
// 预览状态
|
|
56
|
-
const [previewVisible, setPreviewVisible] = useState(false);
|
|
57
|
-
const [previewIndex, setPreviewIndex] = useState(0);
|
|
58
|
-
|
|
59
|
-
// 图片 URL 数组(用于预览组件)
|
|
60
|
-
const imageUrls = useMemo(() => images.map((img) => img.dataUrl), [images]);
|
|
61
|
-
|
|
62
|
-
// 导出格式的图片数据
|
|
63
|
-
const imageData = useMemo<ImageData[]>(
|
|
64
|
-
() =>
|
|
65
|
-
images.map((img) => ({
|
|
66
|
-
base64: img.base64,
|
|
67
|
-
mimeType: img.mimeType,
|
|
68
|
-
})),
|
|
69
|
-
[images]
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// 是否有图片
|
|
73
|
-
const hasImages = images.length > 0;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 压缩图片到指定尺寸
|
|
77
|
-
* @param file 原始图片文件
|
|
78
|
-
* @returns 压缩后的 ImageItem
|
|
79
|
-
*/
|
|
80
|
-
const compressImage = useCallback((file: File): Promise<ImageItem> => {
|
|
81
|
-
return new Promise((resolve, reject) => {
|
|
82
|
-
// 使用 FileReader 读取文件为 data URL(避免 CSP 对 blob URL 的限制)
|
|
83
|
-
const reader = new FileReader();
|
|
84
|
-
|
|
85
|
-
reader.onload = () => {
|
|
86
|
-
const dataUrl = reader.result as string;
|
|
87
|
-
const img = new Image();
|
|
88
|
-
|
|
89
|
-
img.onload = () => {
|
|
90
|
-
let { width, height } = img;
|
|
91
|
-
|
|
92
|
-
// 计算缩放比例
|
|
93
|
-
if (width > maxWidth || height > maxHeight) {
|
|
94
|
-
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
|
95
|
-
width = Math.round(width * ratio);
|
|
96
|
-
height = Math.round(height * ratio);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 创建 canvas 进行压缩
|
|
100
|
-
const canvas = document.createElement('canvas');
|
|
101
|
-
canvas.width = width;
|
|
102
|
-
canvas.height = height;
|
|
103
|
-
|
|
104
|
-
const ctx = canvas.getContext('2d');
|
|
105
|
-
if (!ctx) {
|
|
106
|
-
reject(new Error('无法创建 canvas context'));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
111
|
-
|
|
112
|
-
// 转换为 dataUrl(JPEG 格式以减小体积)
|
|
113
|
-
const mimeType = file.type === 'image/png' ? 'image/png' : 'image/jpeg';
|
|
114
|
-
const compressedDataUrl = canvas.toDataURL(mimeType, quality);
|
|
115
|
-
const base64 = compressedDataUrl.split(',')[1];
|
|
116
|
-
|
|
117
|
-
resolve({
|
|
118
|
-
dataUrl: compressedDataUrl,
|
|
119
|
-
base64,
|
|
120
|
-
mimeType,
|
|
121
|
-
});
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
img.onerror = () => {
|
|
125
|
-
reject(new Error('图片加载失败'));
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
img.src = dataUrl;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
reader.onerror = () => {
|
|
132
|
-
reject(new Error('读取文件失败'));
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
reader.readAsDataURL(file);
|
|
136
|
-
});
|
|
137
|
-
}, [maxWidth, maxHeight, quality]);
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* 读取图片文件为 base64(带压缩)
|
|
141
|
-
*/
|
|
142
|
-
const readImageFile = useCallback((file: File): Promise<ImageItem> => {
|
|
143
|
-
return compressImage(file);
|
|
144
|
-
}, [compressImage]);
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 处理文件列表
|
|
148
|
-
*/
|
|
149
|
-
const processFiles = useCallback(
|
|
150
|
-
async (files: File[]) => {
|
|
151
|
-
const remaining = maxImages - images.length;
|
|
152
|
-
const toProcess = files.slice(0, remaining);
|
|
153
|
-
|
|
154
|
-
const newImages: ImageItem[] = [];
|
|
155
|
-
for (const file of toProcess) {
|
|
156
|
-
if (file.size > maxSize) {
|
|
157
|
-
console.warn(`图片 ${file.name} 超过大小限制 (${Math.round(maxSize / 1024 / 1024)}MB)`);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
const imageItem = await readImageFile(file);
|
|
162
|
-
newImages.push(imageItem);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
console.error('读取图片失败:', err);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (newImages.length > 0) {
|
|
169
|
-
setImages((prev) => [...prev, ...newImages]);
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
[images.length, maxImages, maxSize, readImageFile]
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* 处理文件选择事件
|
|
177
|
-
*/
|
|
178
|
-
const handleImageSelect = useCallback(
|
|
179
|
-
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
180
|
-
const files = event.target.files;
|
|
181
|
-
if (files) {
|
|
182
|
-
processFiles(Array.from(files));
|
|
183
|
-
}
|
|
184
|
-
// 清空 input 以便再次选择相同文件
|
|
185
|
-
event.target.value = '';
|
|
186
|
-
},
|
|
187
|
-
[processFiles]
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* 处理粘贴事件
|
|
192
|
-
*/
|
|
193
|
-
const handlePaste = useCallback(
|
|
194
|
-
(event: React.ClipboardEvent) => {
|
|
195
|
-
const items = event.clipboardData?.items;
|
|
196
|
-
if (!items) return;
|
|
197
|
-
|
|
198
|
-
const imageFiles: File[] = [];
|
|
199
|
-
for (const item of Array.from(items)) {
|
|
200
|
-
if (item.type.startsWith('image/')) {
|
|
201
|
-
const file = item.getAsFile();
|
|
202
|
-
if (file) imageFiles.push(file);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (imageFiles.length > 0) {
|
|
207
|
-
event.preventDefault();
|
|
208
|
-
processFiles(imageFiles);
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
[processFiles]
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* 处理拖拽悬停事件
|
|
216
|
-
*/
|
|
217
|
-
const handleDragOver = useCallback((event: React.DragEvent) => {
|
|
218
|
-
event.preventDefault();
|
|
219
|
-
if (event.dataTransfer?.types.includes('Files')) {
|
|
220
|
-
setIsDragOver(true);
|
|
221
|
-
}
|
|
222
|
-
}, []);
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* 处理拖拽离开事件
|
|
226
|
-
*/
|
|
227
|
-
const handleDragLeave = useCallback(() => {
|
|
228
|
-
setIsDragOver(false);
|
|
229
|
-
}, []);
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* 处理拖拽放下事件
|
|
233
|
-
*/
|
|
234
|
-
const handleDrop = useCallback(
|
|
235
|
-
(event: React.DragEvent) => {
|
|
236
|
-
event.preventDefault();
|
|
237
|
-
setIsDragOver(false);
|
|
238
|
-
const files = event.dataTransfer?.files;
|
|
239
|
-
if (files) {
|
|
240
|
-
const imageFiles = Array.from(files).filter((f) => f.type.startsWith('image/'));
|
|
241
|
-
processFiles(imageFiles);
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
[processFiles]
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* 打开图片预览
|
|
249
|
-
*/
|
|
250
|
-
const openPreview = useCallback((index: number) => {
|
|
251
|
-
setPreviewIndex(index);
|
|
252
|
-
setPreviewVisible(true);
|
|
253
|
-
}, []);
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* 关闭图片预览
|
|
257
|
-
*/
|
|
258
|
-
const closePreview = useCallback(() => {
|
|
259
|
-
setPreviewVisible(false);
|
|
260
|
-
}, []);
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* 移除指定索引的图片
|
|
264
|
-
*/
|
|
265
|
-
const removeImage = useCallback((index: number) => {
|
|
266
|
-
setImages((prev) => prev.filter((_, i) => i !== index));
|
|
267
|
-
}, []);
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* 清空所有图片
|
|
271
|
-
*/
|
|
272
|
-
const clearImages = useCallback(() => {
|
|
273
|
-
setImages([]);
|
|
274
|
-
}, []);
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* 添加图片文件
|
|
278
|
-
*/
|
|
279
|
-
const addImages = useCallback(
|
|
280
|
-
(files: File[]) => {
|
|
281
|
-
processFiles(files);
|
|
282
|
-
},
|
|
283
|
-
[processFiles]
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* 从 data URL 字符串数组初始化图片(用于历史消息编辑)
|
|
288
|
-
* @param data data URL 字符串数组
|
|
289
|
-
*/
|
|
290
|
-
const initImages = useCallback((data: string[]) => {
|
|
291
|
-
const items: ImageItem[] = [];
|
|
292
|
-
for (const item of data) {
|
|
293
|
-
if (item.startsWith('data:')) {
|
|
294
|
-
// data URL 格式: data:image/png;base64,xxxxx
|
|
295
|
-
const matches = item.match(/^data:([^;]+);base64,(.+)$/);
|
|
296
|
-
if (matches) {
|
|
297
|
-
items.push({
|
|
298
|
-
dataUrl: item,
|
|
299
|
-
base64: matches[2],
|
|
300
|
-
mimeType: matches[1],
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
} else {
|
|
304
|
-
// 普通 URL,只能作为预览显示
|
|
305
|
-
items.push({
|
|
306
|
-
dataUrl: item,
|
|
307
|
-
base64: '',
|
|
308
|
-
mimeType: 'image/unknown',
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
setImages(items);
|
|
313
|
-
}, []);
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
// 状态
|
|
317
|
-
images,
|
|
318
|
-
isDragOver,
|
|
319
|
-
previewVisible,
|
|
320
|
-
previewIndex,
|
|
321
|
-
setPreviewIndex,
|
|
322
|
-
|
|
323
|
-
// 计算属性
|
|
324
|
-
imageUrls,
|
|
325
|
-
imageData,
|
|
326
|
-
hasImages,
|
|
327
|
-
|
|
328
|
-
// 事件处理
|
|
329
|
-
handleImageSelect,
|
|
330
|
-
handlePaste,
|
|
331
|
-
handleDragOver,
|
|
332
|
-
handleDragLeave,
|
|
333
|
-
handleDrop,
|
|
334
|
-
|
|
335
|
-
// 预览控制
|
|
336
|
-
openPreview,
|
|
337
|
-
closePreview,
|
|
338
|
-
|
|
339
|
-
// 图片操作
|
|
340
|
-
removeImage,
|
|
341
|
-
clearImages,
|
|
342
|
-
addImages,
|
|
343
|
-
initImages,
|
|
344
|
-
};
|
|
345
|
-
}
|