@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.
Files changed (110) hide show
  1. package/README.md +99 -84
  2. package/dist/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
  3. package/dist/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
  4. package/dist/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
  5. package/dist/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
  6. package/dist/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
  7. package/dist/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
  8. package/dist/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
  9. package/dist/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
  10. package/dist/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
  11. package/dist/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
  12. package/dist/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
  13. package/dist/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
  14. package/dist/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
  15. package/dist/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
  16. package/dist/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
  17. package/dist/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
  18. package/dist/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
  19. package/dist/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
  20. package/dist/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
  21. package/dist/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
  22. package/dist/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
  23. package/dist/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
  24. package/dist/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
  25. package/dist/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
  26. package/dist/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
  27. package/dist/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
  28. package/dist/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
  29. package/dist/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
  30. package/dist/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
  31. package/dist/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
  32. package/dist/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
  33. package/dist/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
  34. package/dist/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
  35. package/dist/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
  36. package/dist/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
  37. package/dist/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
  38. package/dist/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
  39. package/dist/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
  40. package/dist/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
  41. package/dist/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
  42. package/dist/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
  43. package/dist/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
  44. package/dist/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
  45. package/dist/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
  46. package/dist/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
  47. package/dist/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
  48. package/dist/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
  49. package/dist/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
  50. package/dist/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
  51. package/dist/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
  52. package/dist/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
  53. package/dist/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
  54. package/dist/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
  55. package/dist/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
  56. package/dist/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
  57. package/dist/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
  58. package/dist/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
  59. package/dist/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
  60. package/dist/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
  61. package/dist/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
  62. package/dist/index.css +2156 -603
  63. package/dist/index.css.map +1 -1
  64. package/dist/index.d.ts +126 -92
  65. package/dist/index.js +1605 -976
  66. package/dist/index.js.map +1 -1
  67. package/dist/style.css +130 -0
  68. package/package.json +3 -3
  69. package/src/components/ChatPanel.tsx +82 -19
  70. package/src/components/common/SettingsPanel.css +81 -0
  71. package/src/components/common/SettingsPanel.tsx +96 -1
  72. package/src/components/input/ChatInput.css +0 -1
  73. package/src/components/input/ChatInput.tsx +48 -26
  74. package/src/components/input/DropdownSelector.css +66 -0
  75. package/src/components/input/DropdownSelector.tsx +157 -19
  76. package/src/components/message/MessageBubble.css +5 -2
  77. package/src/components/message/MessageBubble.tsx +44 -35
  78. package/src/components/message/PartsRenderer.css +8 -0
  79. package/src/components/message/PartsRenderer.tsx +137 -83
  80. package/src/components/message/parts/CollapsibleCard.css +4 -2
  81. package/src/components/message/parts/CollapsibleCard.tsx +4 -1
  82. package/src/components/message/parts/ImagePart.css +0 -1
  83. package/src/components/message/parts/TextPart.css +574 -5
  84. package/src/components/message/parts/TextPart.tsx +201 -8
  85. package/src/components/message/parts/ToolCallPart.css +139 -115
  86. package/src/components/message/parts/ToolCallPart.tsx +138 -134
  87. package/src/components/message/parts/ToolResultPart.css +0 -1
  88. package/src/components/message/parts/index.ts +3 -1
  89. package/src/components/message/parts/visual-predicate.ts +43 -0
  90. package/src/components/message/parts/visual-render.ts +19 -0
  91. package/src/components/message/parts/visual.ts +12 -0
  92. package/src/context/RenderersContext.tsx +19 -25
  93. package/src/hooks/useChat.ts +567 -79
  94. package/src/hooks/useImageUpload.ts +104 -12
  95. package/src/hooks/useVoiceInput.ts +17 -0
  96. package/src/index.ts +19 -16
  97. package/src/styles.css +130 -0
  98. package/src/types/index.ts +52 -68
  99. package/src/components/message/ContentRenderer.tsx +0 -63
  100. package/src/components/message/ToolResultRenderer.tsx +0 -21
  101. package/src/components/message/blocks/CodeBlock.tsx +0 -60
  102. package/src/components/message/blocks/TextBlock.tsx +0 -15
  103. package/src/components/message/blocks/blocks.css +0 -141
  104. package/src/components/message/blocks/index.ts +0 -6
  105. package/src/components/message/parts/ToolResultPart.tsx +0 -96
  106. package/src/components/message/tool-results/DefaultToolResult.tsx +0 -26
  107. package/src/components/message/tool-results/SearchResults.tsx +0 -69
  108. package/src/components/message/tool-results/WeatherCard.tsx +0 -63
  109. package/src/components/message/tool-results/index.ts +0 -7
  110. 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 { maxImages = 5, maxSize = 10 * 1024 * 1024 } = options;
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
- * 读取图片文件为 base64
76
+ * 压缩图片到指定尺寸
77
+ * @param file 原始图片文件
78
+ * @returns 压缩后的 ImageItem
65
79
  */
66
- const readImageFile = useCallback((file: File): Promise<ImageItem> => {
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
- // dataUrl 格式: data:image/png;base64,xxxxx
72
- const base64 = dataUrl.split(',')[1];
73
- resolve({
74
- dataUrl,
75
- base64,
76
- mimeType: file.type,
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
- reader.onerror = reject;
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
- * - 支持自定义工具结果 UI
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 { RenderersProvider, BlockRenderersContext, ToolRenderersContext } from './context/RenderersContext'
80
- export type { BlockRenderers, ToolRenderers } from './context/RenderersContext'
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
- export { ContentRenderer } from './components/message/ContentRenderer'
93
- export { TextBlock, CodeBlock } from './components/message/blocks'
94
- export { ToolResultRenderer } from './components/message/ToolResultRenderer'
95
- export { DefaultToolResult, WeatherCard, SearchResults } from './components/message/tool-results'
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 CustomWeatherCard from './CustomWeatherCard'
142
- * const toolRenderers = { get_weather: CustomWeatherCard }
143
- * <ChatPanel adapter={adapter} toolRenderers={toolRenderers} />
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
  /* 滚动到底部按钮 */
@@ -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' // pending: 等待用户批准, cancelled: 已取消, skipped: 已跳过
64
- result: unknown | null // 执行结果,null 表示尚未执行或没有结果
65
- }
66
-
67
- /** 工具结果 Part - 用于自定义 UI 渲染(备用,主要使用 ToolCallPart) */
68
- export interface ToolResultPart {
69
- type: 'tool_result'
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
- /** 内容 Part 联合类型 */
92
- export type ContentPart =
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 = ContentPart['type']
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?: Date
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
- }