@huyooo/ai-chat-frontend-react 0.2.12 → 0.2.14
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/README.md +99 -84
- package/dist/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
- package/dist/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
- package/dist/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
- package/dist/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
- package/dist/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
- package/dist/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
- package/dist/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
- package/dist/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
- package/dist/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
- package/dist/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
- package/dist/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
- package/dist/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
- package/dist/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
- package/dist/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
- package/dist/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
- package/dist/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
- package/dist/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
- package/dist/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
- package/dist/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
- package/dist/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
- package/dist/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
- package/dist/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
- package/dist/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
- package/dist/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
- package/dist/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
- package/dist/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
- package/dist/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
- package/dist/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
- package/dist/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
- package/dist/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
- package/dist/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
- package/dist/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
- package/dist/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
- package/dist/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
- package/dist/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
- package/dist/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
- package/dist/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
- package/dist/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
- package/dist/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
- package/dist/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
- package/dist/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
- package/dist/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
- package/dist/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
- package/dist/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
- package/dist/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
- package/dist/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
- package/dist/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
- package/dist/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
- package/dist/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
- package/dist/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
- package/dist/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
- package/dist/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
- package/dist/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
- package/dist/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
- package/dist/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
- package/dist/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
- package/dist/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
- package/dist/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
- package/dist/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
- package/dist/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
- package/dist/index.css +2156 -603
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +126 -92
- package/dist/index.js +1605 -976
- package/dist/index.js.map +1 -1
- package/dist/style.css +130 -0
- package/package.json +3 -3
- package/src/components/ChatPanel.tsx +82 -19
- package/src/components/common/SettingsPanel.css +81 -0
- package/src/components/common/SettingsPanel.tsx +96 -1
- package/src/components/input/ChatInput.css +0 -1
- package/src/components/input/ChatInput.tsx +48 -26
- package/src/components/input/DropdownSelector.css +66 -0
- package/src/components/input/DropdownSelector.tsx +157 -19
- package/src/components/message/MessageBubble.css +5 -2
- package/src/components/message/MessageBubble.tsx +44 -35
- package/src/components/message/PartsRenderer.css +8 -0
- package/src/components/message/PartsRenderer.tsx +137 -83
- package/src/components/message/parts/CollapsibleCard.css +4 -2
- package/src/components/message/parts/CollapsibleCard.tsx +4 -1
- package/src/components/message/parts/ImagePart.css +0 -1
- package/src/components/message/parts/TextPart.css +574 -5
- package/src/components/message/parts/TextPart.tsx +201 -8
- package/src/components/message/parts/ToolCallPart.css +139 -115
- package/src/components/message/parts/ToolCallPart.tsx +138 -134
- package/src/components/message/parts/ToolResultPart.css +0 -1
- package/src/components/message/parts/index.ts +3 -1
- package/src/components/message/parts/visual-predicate.ts +43 -0
- package/src/components/message/parts/visual-render.ts +19 -0
- package/src/components/message/parts/visual.ts +12 -0
- package/src/context/RenderersContext.tsx +19 -25
- package/src/hooks/useChat.ts +567 -79
- package/src/hooks/useImageUpload.ts +104 -12
- package/src/hooks/useVoiceInput.ts +17 -0
- package/src/index.ts +19 -16
- package/src/styles.css +130 -0
- package/src/types/index.ts +52 -68
- package/src/components/message/ContentRenderer.tsx +0 -63
- package/src/components/message/ToolResultRenderer.tsx +0 -21
- package/src/components/message/blocks/CodeBlock.tsx +0 -60
- package/src/components/message/blocks/TextBlock.tsx +0 -15
- package/src/components/message/blocks/blocks.css +0 -141
- package/src/components/message/blocks/index.ts +0 -6
- package/src/components/message/parts/ToolResultPart.tsx +0 -96
- package/src/components/message/tool-results/DefaultToolResult.tsx +0 -26
- package/src/components/message/tool-results/SearchResults.tsx +0 -69
- package/src/components/message/tool-results/WeatherCard.tsx +0 -63
- package/src/components/message/tool-results/index.ts +0 -7
- package/src/components/message/tool-results/tool-results.css +0 -181
|
@@ -25,6 +25,12 @@ interface UseImageUploadOptions {
|
|
|
25
25
|
maxImages?: number;
|
|
26
26
|
/** 单张图片最大大小(字节),默认 10MB */
|
|
27
27
|
maxSize?: number;
|
|
28
|
+
/** 图片最大宽度,默认 4000px(避免超过 API 像素限制) */
|
|
29
|
+
maxWidth?: number;
|
|
30
|
+
/** 图片最大高度,默认 4000px(避免超过 API 像素限制) */
|
|
31
|
+
maxHeight?: number;
|
|
32
|
+
/** 压缩质量 0-1,默认 0.85 */
|
|
33
|
+
quality?: number;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -32,7 +38,13 @@ interface UseImageUploadOptions {
|
|
|
32
38
|
* @param options 配置项
|
|
33
39
|
*/
|
|
34
40
|
export function useImageUpload(options: UseImageUploadOptions = {}) {
|
|
35
|
-
const {
|
|
41
|
+
const {
|
|
42
|
+
maxImages = 5,
|
|
43
|
+
maxSize = 10 * 1024 * 1024,
|
|
44
|
+
maxWidth = 4000,
|
|
45
|
+
maxHeight = 4000,
|
|
46
|
+
quality = 0.85
|
|
47
|
+
} = options;
|
|
36
48
|
|
|
37
49
|
// 图片列表
|
|
38
50
|
const [images, setImages] = useState<ImageItem[]>([]);
|
|
@@ -61,25 +73,75 @@ export function useImageUpload(options: UseImageUploadOptions = {}) {
|
|
|
61
73
|
const hasImages = images.length > 0;
|
|
62
74
|
|
|
63
75
|
/**
|
|
64
|
-
*
|
|
76
|
+
* 压缩图片到指定尺寸
|
|
77
|
+
* @param file 原始图片文件
|
|
78
|
+
* @returns 压缩后的 ImageItem
|
|
65
79
|
*/
|
|
66
|
-
const
|
|
80
|
+
const compressImage = useCallback((file: File): Promise<ImageItem> => {
|
|
67
81
|
return new Promise((resolve, reject) => {
|
|
82
|
+
// 使用 FileReader 读取文件为 data URL(避免 CSP 对 blob URL 的限制)
|
|
68
83
|
const reader = new FileReader();
|
|
84
|
+
|
|
69
85
|
reader.onload = () => {
|
|
70
86
|
const dataUrl = reader.result as string;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
|
78
129
|
};
|
|
79
|
-
|
|
130
|
+
|
|
131
|
+
reader.onerror = () => {
|
|
132
|
+
reject(new Error('读取文件失败'));
|
|
133
|
+
};
|
|
134
|
+
|
|
80
135
|
reader.readAsDataURL(file);
|
|
81
136
|
});
|
|
82
|
-
}, []);
|
|
137
|
+
}, [maxWidth, maxHeight, quality]);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 读取图片文件为 base64(带压缩)
|
|
141
|
+
*/
|
|
142
|
+
const readImageFile = useCallback((file: File): Promise<ImageItem> => {
|
|
143
|
+
return compressImage(file);
|
|
144
|
+
}, [compressImage]);
|
|
83
145
|
|
|
84
146
|
/**
|
|
85
147
|
* 处理文件列表
|
|
@@ -221,6 +283,35 @@ export function useImageUpload(options: UseImageUploadOptions = {}) {
|
|
|
221
283
|
[processFiles]
|
|
222
284
|
);
|
|
223
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
|
+
|
|
224
315
|
return {
|
|
225
316
|
// 状态
|
|
226
317
|
images,
|
|
@@ -249,5 +340,6 @@ export function useImageUpload(options: UseImageUploadOptions = {}) {
|
|
|
249
340
|
removeImage,
|
|
250
341
|
clearImages,
|
|
251
342
|
addImages,
|
|
343
|
+
initImages,
|
|
252
344
|
};
|
|
253
345
|
}
|
|
@@ -55,6 +55,9 @@ function resample(audioData: Float32Array, fromSampleRate: number, toSampleRate:
|
|
|
55
55
|
return result;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// 全局预热标志:确保只预热一次(避免多个组件实例重复预热)
|
|
59
|
+
let asrWarmupDone = false;
|
|
60
|
+
|
|
58
61
|
async function setupAudioWorkletCapture(opts: {
|
|
59
62
|
audioContext: AudioContext;
|
|
60
63
|
source: MediaStreamAudioSourceNode;
|
|
@@ -156,6 +159,20 @@ registerProcessor('pcm-capture', PcmCaptureProcessor);
|
|
|
156
159
|
export function useVoiceInput(adapter: ChatAdapter | undefined, config: VoiceInputConfig = {}): UseVoiceInputReturn {
|
|
157
160
|
const { sampleRate = 16000, sendInterval = 200, enablePunc = true, enableItn = true } = config;
|
|
158
161
|
|
|
162
|
+
// 自动预热 ASR 连接(仅首次调用,延迟执行,避免阻塞初始化)
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (adapter && !asrWarmupDone && typeof adapter.asrWarmup === 'function') {
|
|
165
|
+
asrWarmupDone = true;
|
|
166
|
+
// 延迟 800ms 预热,避免与首屏渲染竞争资源
|
|
167
|
+
const timer = setTimeout(() => {
|
|
168
|
+
adapter.asrWarmup?.().catch(() => {
|
|
169
|
+
// 静默失败,不影响功能
|
|
170
|
+
});
|
|
171
|
+
}, 800);
|
|
172
|
+
return () => clearTimeout(timer);
|
|
173
|
+
}
|
|
174
|
+
}, [adapter]);
|
|
175
|
+
|
|
159
176
|
const [status, setStatus] = useState<VoiceInputStatus>('idle');
|
|
160
177
|
const [currentText, setCurrentText] = useState('');
|
|
161
178
|
const [finalText, setFinalText] = useState('');
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 新架构:使用 ContentPart 数组渲染消息内容
|
|
7
7
|
* - 支持流式渲染
|
|
8
|
-
* -
|
|
8
|
+
* - 支持自定义 Part 类型渲染(如 weather, stock)
|
|
9
9
|
* - 支持思考、搜索、工具调用等多种内容类型
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -51,7 +51,6 @@ export type {
|
|
|
51
51
|
ThinkingPart,
|
|
52
52
|
SearchPart,
|
|
53
53
|
ToolCallPart,
|
|
54
|
-
ToolResultPart,
|
|
55
54
|
ImagePart,
|
|
56
55
|
ErrorPart,
|
|
57
56
|
// 搜索结果
|
|
@@ -75,9 +74,9 @@ export type { UseChatOptions, ToolCompleteEvent, SideEffect } from './hooks/useC
|
|
|
75
74
|
export { ChatInputProvider, useChatInputContext } from './context/ChatInputContext'
|
|
76
75
|
export type { ChatInputContextValue } from './context/ChatInputContext'
|
|
77
76
|
|
|
78
|
-
// 渲染器上下文
|
|
79
|
-
export {
|
|
80
|
-
export type {
|
|
77
|
+
// Part 渲染器上下文
|
|
78
|
+
export { PartRenderersProvider, PartRenderersContext } from './context/RenderersContext'
|
|
79
|
+
export type { PartRenderers, PartRendererProps } from './context/RenderersContext'
|
|
81
80
|
|
|
82
81
|
// ==================== 主组件 ====================
|
|
83
82
|
|
|
@@ -87,12 +86,17 @@ export type { ChatPanelHandle } from './components/ChatPanel'
|
|
|
87
86
|
// ==================== 消息组件 ====================
|
|
88
87
|
|
|
89
88
|
export { MessageBubble } from './components/message/MessageBubble'
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
export {
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
export { PartsRenderer } from './components/message/PartsRenderer'
|
|
90
|
+
|
|
91
|
+
// Part 渲染组件
|
|
92
|
+
export {
|
|
93
|
+
TextPart as TextPartComponent,
|
|
94
|
+
ThinkingPart as ThinkingPartComponent,
|
|
95
|
+
SearchPart as SearchPartComponent,
|
|
96
|
+
ToolCallPart as ToolCallPartComponent,
|
|
97
|
+
ImagePart as ImagePartComponent,
|
|
98
|
+
ErrorPart as ErrorPartComponent,
|
|
99
|
+
} from './components/message/parts'
|
|
96
100
|
|
|
97
101
|
// ==================== 其他组件 ====================
|
|
98
102
|
|
|
@@ -119,7 +123,6 @@ export type {
|
|
|
119
123
|
ContentBlock,
|
|
120
124
|
TextBlock as TextBlockType,
|
|
121
125
|
CodeBlock as CodeBlockType,
|
|
122
|
-
ToolRendererProps,
|
|
123
126
|
WeatherData,
|
|
124
127
|
SearchResultItem,
|
|
125
128
|
} from '@huyooo/ai-chat-shared'
|
|
@@ -137,10 +140,10 @@ export { parseContent, highlightCode, getLanguageDisplayName, renderMarkdown } f
|
|
|
137
140
|
* const adapter = createElectronAdapter()
|
|
138
141
|
* <ChatPanel adapter={adapter} cwd="/path/to/dir" />
|
|
139
142
|
*
|
|
140
|
-
* 3.
|
|
141
|
-
* import
|
|
142
|
-
* const
|
|
143
|
-
* <ChatPanel adapter={adapter}
|
|
143
|
+
* 3. 自定义 Part 渲染器(新架构):
|
|
144
|
+
* import WeatherCard from './WeatherCard'
|
|
145
|
+
* const partRenderers = { weather: WeatherCard }
|
|
146
|
+
* <ChatPanel adapter={adapter} partRenderers={partRenderers} />
|
|
144
147
|
*
|
|
145
148
|
* 4. 使用 useChat hook 自定义 UI:
|
|
146
149
|
* import { useChat } from '@huyooo/ai-chat-frontend-react'
|
package/src/styles.css
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* 仅包含 CSS 变量和 ChatPanel 基础布局
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/* 导入 Markdown / Mermaid 等共享渲染样式(与 vue 版本保持一致) */
|
|
7
|
+
@import "@huyooo/ai-chat-shared/styles";
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* 主题协议(跨项目统一):
|
|
8
11
|
* - 使用 document.documentElement 的 data-theme = light | dark
|
|
@@ -25,6 +28,7 @@
|
|
|
25
28
|
--chat-muted-hover: #e6e5e0;
|
|
26
29
|
--chat-border: #26251e1a;
|
|
27
30
|
--chat-text: #26251eeb;
|
|
31
|
+
--chat-text-strong: #26251e;
|
|
28
32
|
--chat-text-muted: #26251e99;
|
|
29
33
|
/* 主色:light/dark 保持一致(蓝) */
|
|
30
34
|
--chat-primary: #54a9ff;
|
|
@@ -41,6 +45,36 @@
|
|
|
41
45
|
--chat-fab-bg: #ffffff;
|
|
42
46
|
--chat-fab-bg-hover: var(--chat-input-bg);
|
|
43
47
|
--chat-fab-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
|
|
48
|
+
|
|
49
|
+
/* 代码高亮 - GitHub Light 风格 */
|
|
50
|
+
--chat-hljs-keyword: #cf222e;
|
|
51
|
+
--chat-hljs-built-in: #953800;
|
|
52
|
+
--chat-hljs-type: #953800;
|
|
53
|
+
--chat-hljs-function: #8250df;
|
|
54
|
+
--chat-hljs-string: #0a3069;
|
|
55
|
+
--chat-hljs-number: #0550ae;
|
|
56
|
+
--chat-hljs-literal: #0550ae;
|
|
57
|
+
--chat-hljs-comment: #6e7781;
|
|
58
|
+
--chat-hljs-variable: #953800;
|
|
59
|
+
--chat-hljs-attr: #0550ae;
|
|
60
|
+
--chat-hljs-property: #116329;
|
|
61
|
+
--chat-hljs-operator: #cf222e;
|
|
62
|
+
--chat-hljs-punctuation: #24292f;
|
|
63
|
+
--chat-hljs-params: #24292f;
|
|
64
|
+
--chat-hljs-regexp: #116329;
|
|
65
|
+
--chat-hljs-selector: #116329;
|
|
66
|
+
--chat-hljs-tag: #116329;
|
|
67
|
+
--chat-hljs-name: #116329;
|
|
68
|
+
--chat-hljs-deletion: #82071e;
|
|
69
|
+
--chat-hljs-deletion-bg: rgba(255, 129, 130, 0.15);
|
|
70
|
+
--chat-hljs-addition: #116329;
|
|
71
|
+
--chat-hljs-addition-bg: rgba(46, 160, 67, 0.15);
|
|
72
|
+
--chat-hljs-meta: #6e7781;
|
|
73
|
+
--chat-hljs-link: #0a3069;
|
|
74
|
+
--chat-hljs-symbol: #0550ae;
|
|
75
|
+
--chat-hljs-subst: #24292f;
|
|
76
|
+
--chat-hljs-section: #0550ae;
|
|
77
|
+
--chat-hljs-bullet: #953800;
|
|
44
78
|
}
|
|
45
79
|
|
|
46
80
|
/* 默认跟随系统暗色 */
|
|
@@ -56,6 +90,7 @@
|
|
|
56
90
|
--chat-muted-hover: #3c3c3c;
|
|
57
91
|
--chat-border: #333;
|
|
58
92
|
--chat-text: #ccc;
|
|
93
|
+
--chat-text-strong: #fff;
|
|
59
94
|
--chat-text-muted: #888;
|
|
60
95
|
/* 主色:light/dark 保持一致(蓝) */
|
|
61
96
|
--chat-primary: #54a9ff;
|
|
@@ -71,6 +106,36 @@
|
|
|
71
106
|
--chat-fab-bg: var(--chat-muted);
|
|
72
107
|
--chat-fab-bg-hover: var(--chat-muted-hover);
|
|
73
108
|
--chat-fab-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
109
|
+
|
|
110
|
+
/* 代码高亮 - GitHub Dark 风格 */
|
|
111
|
+
--chat-hljs-keyword: #ff7b72;
|
|
112
|
+
--chat-hljs-built-in: #ffa657;
|
|
113
|
+
--chat-hljs-type: #ffa657;
|
|
114
|
+
--chat-hljs-function: #d2a8ff;
|
|
115
|
+
--chat-hljs-string: #a5d6ff;
|
|
116
|
+
--chat-hljs-number: #79c0ff;
|
|
117
|
+
--chat-hljs-literal: #79c0ff;
|
|
118
|
+
--chat-hljs-comment: #8b949e;
|
|
119
|
+
--chat-hljs-variable: #ffa657;
|
|
120
|
+
--chat-hljs-attr: #79c0ff;
|
|
121
|
+
--chat-hljs-property: #7ee787;
|
|
122
|
+
--chat-hljs-operator: #ff7b72;
|
|
123
|
+
--chat-hljs-punctuation: #e6edf3;
|
|
124
|
+
--chat-hljs-params: #e6edf3;
|
|
125
|
+
--chat-hljs-regexp: #7ee787;
|
|
126
|
+
--chat-hljs-selector: #7ee787;
|
|
127
|
+
--chat-hljs-tag: #7ee787;
|
|
128
|
+
--chat-hljs-name: #7ee787;
|
|
129
|
+
--chat-hljs-deletion: #ffa198;
|
|
130
|
+
--chat-hljs-deletion-bg: rgba(248, 81, 73, 0.15);
|
|
131
|
+
--chat-hljs-addition: #7ee787;
|
|
132
|
+
--chat-hljs-addition-bg: rgba(46, 160, 67, 0.15);
|
|
133
|
+
--chat-hljs-meta: #8b949e;
|
|
134
|
+
--chat-hljs-link: #a5d6ff;
|
|
135
|
+
--chat-hljs-symbol: #79c0ff;
|
|
136
|
+
--chat-hljs-subst: #e6edf3;
|
|
137
|
+
--chat-hljs-section: #79c0ff;
|
|
138
|
+
--chat-hljs-bullet: #ffa657;
|
|
74
139
|
}
|
|
75
140
|
}
|
|
76
141
|
|
|
@@ -86,6 +151,7 @@
|
|
|
86
151
|
--chat-muted-hover: #e6e5e0;
|
|
87
152
|
--chat-border: #26251e1a;
|
|
88
153
|
--chat-text: #26251eeb;
|
|
154
|
+
--chat-text-strong: #26251e;
|
|
89
155
|
--chat-text-muted: #26251e99;
|
|
90
156
|
--chat-primary: #54a9ff;
|
|
91
157
|
--chat-primary-hover: #2f90ff;
|
|
@@ -99,6 +165,36 @@
|
|
|
99
165
|
--chat-fab-bg: #ffffff;
|
|
100
166
|
--chat-fab-bg-hover: var(--chat-input-bg);
|
|
101
167
|
--chat-fab-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
|
|
168
|
+
|
|
169
|
+
/* 代码高亮 - GitHub Light 风格 */
|
|
170
|
+
--chat-hljs-keyword: #cf222e;
|
|
171
|
+
--chat-hljs-built-in: #953800;
|
|
172
|
+
--chat-hljs-type: #953800;
|
|
173
|
+
--chat-hljs-function: #8250df;
|
|
174
|
+
--chat-hljs-string: #0a3069;
|
|
175
|
+
--chat-hljs-number: #0550ae;
|
|
176
|
+
--chat-hljs-literal: #0550ae;
|
|
177
|
+
--chat-hljs-comment: #6e7781;
|
|
178
|
+
--chat-hljs-variable: #953800;
|
|
179
|
+
--chat-hljs-attr: #0550ae;
|
|
180
|
+
--chat-hljs-property: #116329;
|
|
181
|
+
--chat-hljs-operator: #cf222e;
|
|
182
|
+
--chat-hljs-punctuation: #24292f;
|
|
183
|
+
--chat-hljs-params: #24292f;
|
|
184
|
+
--chat-hljs-regexp: #116329;
|
|
185
|
+
--chat-hljs-selector: #116329;
|
|
186
|
+
--chat-hljs-tag: #116329;
|
|
187
|
+
--chat-hljs-name: #116329;
|
|
188
|
+
--chat-hljs-deletion: #82071e;
|
|
189
|
+
--chat-hljs-deletion-bg: rgba(255, 129, 130, 0.15);
|
|
190
|
+
--chat-hljs-addition: #116329;
|
|
191
|
+
--chat-hljs-addition-bg: rgba(46, 160, 67, 0.15);
|
|
192
|
+
--chat-hljs-meta: #6e7781;
|
|
193
|
+
--chat-hljs-link: #0a3069;
|
|
194
|
+
--chat-hljs-symbol: #0550ae;
|
|
195
|
+
--chat-hljs-subst: #24292f;
|
|
196
|
+
--chat-hljs-section: #0550ae;
|
|
197
|
+
--chat-hljs-bullet: #953800;
|
|
102
198
|
}
|
|
103
199
|
|
|
104
200
|
:root[data-theme="dark"] {
|
|
@@ -112,6 +208,7 @@
|
|
|
112
208
|
--chat-muted-hover: #3c3c3c;
|
|
113
209
|
--chat-border: #333;
|
|
114
210
|
--chat-text: #ccc;
|
|
211
|
+
--chat-text-strong: #fff;
|
|
115
212
|
--chat-text-muted: #888;
|
|
116
213
|
/* 主色:light/dark 保持一致(蓝) */
|
|
117
214
|
--chat-primary: #54a9ff;
|
|
@@ -126,6 +223,36 @@
|
|
|
126
223
|
--chat-fab-bg: var(--chat-muted);
|
|
127
224
|
--chat-fab-bg-hover: var(--chat-muted-hover);
|
|
128
225
|
--chat-fab-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
226
|
+
|
|
227
|
+
/* 代码高亮 - GitHub Dark 风格 */
|
|
228
|
+
--chat-hljs-keyword: #ff7b72;
|
|
229
|
+
--chat-hljs-built-in: #ffa657;
|
|
230
|
+
--chat-hljs-type: #ffa657;
|
|
231
|
+
--chat-hljs-function: #d2a8ff;
|
|
232
|
+
--chat-hljs-string: #a5d6ff;
|
|
233
|
+
--chat-hljs-number: #79c0ff;
|
|
234
|
+
--chat-hljs-literal: #79c0ff;
|
|
235
|
+
--chat-hljs-comment: #8b949e;
|
|
236
|
+
--chat-hljs-variable: #ffa657;
|
|
237
|
+
--chat-hljs-attr: #79c0ff;
|
|
238
|
+
--chat-hljs-property: #7ee787;
|
|
239
|
+
--chat-hljs-operator: #ff7b72;
|
|
240
|
+
--chat-hljs-punctuation: #e6edf3;
|
|
241
|
+
--chat-hljs-params: #e6edf3;
|
|
242
|
+
--chat-hljs-regexp: #7ee787;
|
|
243
|
+
--chat-hljs-selector: #7ee787;
|
|
244
|
+
--chat-hljs-tag: #7ee787;
|
|
245
|
+
--chat-hljs-name: #7ee787;
|
|
246
|
+
--chat-hljs-deletion: #ffa198;
|
|
247
|
+
--chat-hljs-deletion-bg: rgba(248, 81, 73, 0.15);
|
|
248
|
+
--chat-hljs-addition: #7ee787;
|
|
249
|
+
--chat-hljs-addition-bg: rgba(46, 160, 67, 0.15);
|
|
250
|
+
--chat-hljs-meta: #8b949e;
|
|
251
|
+
--chat-hljs-link: #a5d6ff;
|
|
252
|
+
--chat-hljs-symbol: #79c0ff;
|
|
253
|
+
--chat-hljs-subst: #e6edf3;
|
|
254
|
+
--chat-hljs-section: #79c0ff;
|
|
255
|
+
--chat-hljs-bullet: #ffa657;
|
|
129
256
|
}
|
|
130
257
|
|
|
131
258
|
/* 统一滚动条样式 - 任何需要自定义滚动条的元素添加此类 */
|
|
@@ -170,6 +297,9 @@
|
|
|
170
297
|
overflow-y: auto;
|
|
171
298
|
padding: 12px;
|
|
172
299
|
scroll-behavior: smooth;
|
|
300
|
+
display: flex;
|
|
301
|
+
flex-direction: column;
|
|
302
|
+
gap: 8px;
|
|
173
303
|
}
|
|
174
304
|
|
|
175
305
|
/* 滚动到底部按钮 */
|
package/src/types/index.ts
CHANGED
|
@@ -24,6 +24,12 @@ export type ModelOption = ModelOptionType
|
|
|
24
24
|
export type ProviderType = ProviderTypeType
|
|
25
25
|
|
|
26
26
|
// ==================== Content Part 类型 ====================
|
|
27
|
+
//
|
|
28
|
+
// 架构说明:
|
|
29
|
+
// 1. 内置类型:text, code, thinking, search, tool_call, image, error
|
|
30
|
+
// 2. 扩展类型:weather, stock 等业务类型(通过 partRenderers 注册渲染器)
|
|
31
|
+
// 3. 工具执行完成后,如果定义了 resultType,直接生成对应类型的 Part(如 weather)
|
|
32
|
+
// 4. 前端通过 partRenderers 统一渲染所有 Part 类型
|
|
27
33
|
|
|
28
34
|
/** 搜索结果 */
|
|
29
35
|
export interface SearchResult {
|
|
@@ -38,6 +44,13 @@ export interface TextPart {
|
|
|
38
44
|
text: string
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
/** 代码块 Part(从 text 中解析,或直接返回)*/
|
|
48
|
+
export interface CodePart {
|
|
49
|
+
type: 'code'
|
|
50
|
+
content: string
|
|
51
|
+
language?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/** 思考过程 Part */
|
|
42
55
|
export interface ThinkingPart {
|
|
43
56
|
type: 'thinking'
|
|
@@ -54,24 +67,19 @@ export interface SearchPart {
|
|
|
54
67
|
status: 'running' | 'done'
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
/** 工具调用 Part
|
|
70
|
+
/** 工具调用 Part(仅展示执行过程,结果由具体类型 Part 渲染)*/
|
|
58
71
|
export interface ToolCallPart {
|
|
59
72
|
type: 'tool_call'
|
|
60
73
|
id: string
|
|
61
74
|
name: string
|
|
62
75
|
args?: Record<string, unknown>
|
|
63
|
-
status: 'pending' | 'running' | 'done' | 'error' | 'cancelled' | 'skipped'
|
|
64
|
-
result
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
id: string
|
|
71
|
-
name: string
|
|
72
|
-
args?: Record<string, unknown>
|
|
73
|
-
result: unknown // 解析后的结构化数据
|
|
74
|
-
status: 'done' | 'error' | 'cancelled' | 'skipped'
|
|
76
|
+
status: 'pending' | 'running' | 'done' | 'error' | 'cancelled' | 'skipped'
|
|
77
|
+
// 注意:不再有 result 字段,结果由工具的 resultType 指定的 Part 类型渲染
|
|
78
|
+
/** 工具执行输出(用于 execute_command 等需要展示 stdout/stderr 的工具) */
|
|
79
|
+
output?: {
|
|
80
|
+
stdout?: string
|
|
81
|
+
stderr?: string
|
|
82
|
+
}
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/** 图片 Part */
|
|
@@ -88,18 +96,44 @@ export interface ErrorPart {
|
|
|
88
96
|
retryable?: boolean
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
// ==================== 扩展 Part 类型(业务类型)====================
|
|
100
|
+
|
|
101
|
+
/** 天气 Part(由 get_weather 工具生成)*/
|
|
102
|
+
export interface WeatherPart {
|
|
103
|
+
type: 'weather'
|
|
104
|
+
city: string
|
|
105
|
+
temperature: number
|
|
106
|
+
condition: string
|
|
107
|
+
humidity: number
|
|
108
|
+
wind: string
|
|
109
|
+
reportTime?: string
|
|
110
|
+
province?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 自定义 Part 基础接口(用于扩展)*/
|
|
114
|
+
export interface CustomPart {
|
|
115
|
+
type: string
|
|
116
|
+
[key: string]: unknown
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 内置 Part 联合类型 */
|
|
120
|
+
export type BuiltinPart =
|
|
93
121
|
| TextPart
|
|
122
|
+
| CodePart
|
|
94
123
|
| ThinkingPart
|
|
95
124
|
| SearchPart
|
|
96
125
|
| ToolCallPart
|
|
97
|
-
| ToolResultPart
|
|
98
126
|
| ImagePart
|
|
99
127
|
| ErrorPart
|
|
100
128
|
|
|
129
|
+
/** 内容 Part 联合类型(包含内置和扩展类型)*/
|
|
130
|
+
export type ContentPart = BuiltinPart | WeatherPart | CustomPart
|
|
131
|
+
|
|
132
|
+
/** 内置 Part 类型字符串 */
|
|
133
|
+
export type BuiltinPartType = BuiltinPart['type']
|
|
134
|
+
|
|
101
135
|
/** 内容 Part 类型字符串 */
|
|
102
|
-
export type ContentPartType =
|
|
136
|
+
export type ContentPartType = string
|
|
103
137
|
|
|
104
138
|
/** 步骤折叠模式 */
|
|
105
139
|
export type StepsExpandedType = 'open' | 'close' | 'auto'
|
|
@@ -137,7 +171,7 @@ export interface ChatMessage {
|
|
|
137
171
|
/** 是否已复制 */
|
|
138
172
|
copied?: boolean
|
|
139
173
|
/** 消息时间戳 */
|
|
140
|
-
timestamp?:
|
|
174
|
+
timestamp?: number
|
|
141
175
|
/** 错误详情(如果有错误) */
|
|
142
176
|
error?: ErrorDetails
|
|
143
177
|
/** 是否被用户中止 */
|
|
@@ -160,53 +194,3 @@ export interface ChatInputOptions {
|
|
|
160
194
|
thinkingEnabled: boolean
|
|
161
195
|
}
|
|
162
196
|
|
|
163
|
-
// ==================== 兼容旧类型(逐步废弃)====================
|
|
164
|
-
|
|
165
|
-
/** @deprecated 使用 ContentPart 替代 */
|
|
166
|
-
export type ExecutionStepType = 'thinking' | 'search' | 'tool_call' | 'error'
|
|
167
|
-
|
|
168
|
-
/** @deprecated 使用 ContentPart 替代 */
|
|
169
|
-
export type ExecutionStepStatus = 'running' | 'completed' | 'error'
|
|
170
|
-
|
|
171
|
-
/** @deprecated 使用 ContentPart 替代 */
|
|
172
|
-
export type ExecutionStep = {
|
|
173
|
-
id: string
|
|
174
|
-
type: ExecutionStepType
|
|
175
|
-
status: ExecutionStepStatus
|
|
176
|
-
startedAt?: number
|
|
177
|
-
completedAt?: number
|
|
178
|
-
duration?: number
|
|
179
|
-
content?: string
|
|
180
|
-
query?: string
|
|
181
|
-
results?: SearchResult[]
|
|
182
|
-
callId?: string
|
|
183
|
-
name?: string
|
|
184
|
-
args?: Record<string, unknown>
|
|
185
|
-
result?: string
|
|
186
|
-
message?: string
|
|
187
|
-
category?: string
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** @deprecated 使用 ContentPart 替代 - 思考步骤类型 */
|
|
191
|
-
export type ThinkingStep = ExecutionStep & {
|
|
192
|
-
type: 'thinking'
|
|
193
|
-
status: 'running' | 'completed'
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** @deprecated 使用 ContentPart 替代 - 搜索步骤类型 */
|
|
197
|
-
export type SearchStep = ExecutionStep & {
|
|
198
|
-
type: 'search'
|
|
199
|
-
status: 'running' | 'completed'
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** @deprecated 使用 ContentPart 替代 - 工具调用步骤类型 */
|
|
203
|
-
export type ToolCallStep = ExecutionStep & {
|
|
204
|
-
type: 'tool_call'
|
|
205
|
-
status: 'running' | 'completed' | 'error'
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** @deprecated 使用 ContentPart 替代 - 错误步骤类型 */
|
|
209
|
-
export type ErrorStep = ExecutionStep & {
|
|
210
|
-
type: 'error'
|
|
211
|
-
status: 'error'
|
|
212
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 内容渲染器
|
|
3
|
-
* 将原始文本解析为内容块并渲染
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { FC, useMemo, useContext, type ComponentType } from 'react'
|
|
7
|
-
import { parseContent } from '@huyooo/ai-chat-shared'
|
|
8
|
-
import type { ContentBlock } from '@huyooo/ai-chat-shared'
|
|
9
|
-
import { TextBlock, CodeBlock } from './blocks'
|
|
10
|
-
import { BlockRenderersContext } from '../../context/RenderersContext'
|
|
11
|
-
import './ContentRenderer.css'
|
|
12
|
-
|
|
13
|
-
interface ContentRendererProps {
|
|
14
|
-
/** 原始文本内容 */
|
|
15
|
-
content: string
|
|
16
|
-
/** 预解析的块列表(可选,用于流式更新) */
|
|
17
|
-
blocks?: ContentBlock[]
|
|
18
|
-
/** 代码复制事件 */
|
|
19
|
-
onCodeCopy?: (code: string) => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const ContentRenderer: FC<ContentRendererProps> = ({
|
|
23
|
-
content,
|
|
24
|
-
blocks: preBlocks,
|
|
25
|
-
onCodeCopy,
|
|
26
|
-
}) => {
|
|
27
|
-
// 从上层获取自定义块渲染器
|
|
28
|
-
const customRenderers = useContext(BlockRenderersContext)
|
|
29
|
-
|
|
30
|
-
// 解析后的内容块
|
|
31
|
-
const blocks = useMemo(() => {
|
|
32
|
-
// 优先使用预解析的块
|
|
33
|
-
if (preBlocks?.length) {
|
|
34
|
-
return preBlocks
|
|
35
|
-
}
|
|
36
|
-
// 否则解析原始内容
|
|
37
|
-
return parseContent(content)
|
|
38
|
-
}, [content, preBlocks])
|
|
39
|
-
|
|
40
|
-
if (!blocks.length) return null
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div className="content-renderer">
|
|
44
|
-
{blocks.map((block) => {
|
|
45
|
-
// 自定义块渲染器
|
|
46
|
-
const CustomRenderer = customRenderers[block.type] as ComponentType<{ block: ContentBlock }>
|
|
47
|
-
if (CustomRenderer) {
|
|
48
|
-
return <CustomRenderer key={block.id} block={block} />
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 内置渲染器
|
|
52
|
-
switch (block.type) {
|
|
53
|
-
case 'text':
|
|
54
|
-
return <TextBlock key={block.id} block={block} />
|
|
55
|
-
case 'code':
|
|
56
|
-
return <CodeBlock key={block.id} block={block} onCopy={onCodeCopy} />
|
|
57
|
-
default:
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
})}
|
|
61
|
-
</div>
|
|
62
|
-
)
|
|
63
|
-
}
|