@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
package/dist/style.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huyooo/ai-chat-frontend-react",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "AI Chat Frontend - React components with adapter pattern",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,8 +32,8 @@
32
32
  "react-dom": ">=18.0.0"
33
33
  },
34
34
  "dependencies": {
35
- "@huyooo/ai-chat-bridge-electron": "^0.2.12",
36
- "@huyooo/ai-chat-shared": "^0.2.12",
35
+ "@huyooo/ai-chat-bridge-electron": "^0.2.14",
36
+ "@huyooo/ai-chat-shared": "^0.2.14",
37
37
  "@iconify/react": "^5.0.2"
38
38
  },
39
39
  "devDependencies": {
@@ -6,16 +6,16 @@
6
6
  import { useEffect, useRef, useCallback, useMemo, useState, forwardRef, useImperativeHandle, type ComponentType } from 'react'
7
7
  import { Icon } from '@iconify/react'
8
8
  import { useChat, type ToolCompleteEvent } from '../hooks/useChat'
9
+ import type { ImageData } from '../adapter'
9
10
  import type { ChatAdapter, ModelOption, ChatMode } from '@huyooo/ai-chat-bridge-electron/renderer'
10
11
  import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
11
- import type { ToolRendererProps } from '@huyooo/ai-chat-shared'
12
12
  import { ChatHeader } from './header/ChatHeader'
13
13
  import { WelcomeMessage } from './message/WelcomeMessage'
14
14
  import type { WelcomeConfig } from './message/welcome-types'
15
15
  import { MessageBubble } from './message/MessageBubble'
16
16
  import { ChatInput, type ChatInputHandle } from './input/ChatInput'
17
17
  import { ChatInputProvider } from '../context/ChatInputContext'
18
- import { RenderersProvider } from '../context/RenderersContext'
18
+ import { PartRenderersProvider, type PartRenderers } from '../context/RenderersContext'
19
19
  import { ConfirmDialog } from './common/ConfirmDialog'
20
20
  import { Toast } from './common/Toast'
21
21
  // ToolApprovalDialog 已移除,工具批准现在内嵌在 ToolCallPart 中
@@ -54,8 +54,8 @@ interface ChatPanelProps {
54
54
  className?: string
55
55
  /** 欢迎页配置 */
56
56
  welcomeConfig?: Partial<WelcomeConfig>
57
- /** 自定义工具结果渲染器 - 根据工具名称选择渲染组件 */
58
- toolRenderers?: Record<string, ComponentType<ToolRendererProps>>
57
+ /** 自定义 Part 渲染器 - 根据 part.type 选择渲染组件(如 weather, stock 等) */
58
+ partRenderers?: PartRenderers
59
59
  /**
60
60
  * 执行步骤折叠模式
61
61
  * - 'open': 始终展开
@@ -75,7 +75,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
75
75
  onToolComplete,
76
76
  className = '',
77
77
  welcomeConfig,
78
- toolRenderers = {},
78
+ partRenderers = {},
79
79
  stepsExpandedType = 'auto',
80
80
  }, ref) => {
81
81
  const messagesRef = useRef<HTMLDivElement>(null)
@@ -83,7 +83,12 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
83
83
 
84
84
  // 是否应该自动滚动(用户在底部附近时才自动滚动)
85
85
  const [shouldAutoScroll, setShouldAutoScroll] = useState(true)
86
- const SCROLL_THRESHOLD = 100
86
+ // 距离底部多少像素内算"在底部"
87
+ const SCROLL_THRESHOLD = 25
88
+ // 上次滚动位置(用于检测滚动方向)
89
+ const lastScrollTopRef = useRef(0)
90
+ // 是否正在程序化滚动(用于区分用户手动滚动)
91
+ const isProgrammaticScrollRef = useRef(false)
87
92
 
88
93
  // 设置面板状态
89
94
  const [settingsPanelVisible, setSettingsPanelVisible] = useState(false)
@@ -159,6 +164,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
159
164
  cancelRequest,
160
165
  copyMessage,
161
166
  regenerateMessage,
167
+ resendFromIndex,
162
168
  setMode,
163
169
  setModel,
164
170
  setWebSearch,
@@ -166,6 +172,10 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
166
172
  setWorkingDirectory,
167
173
  autoRunConfig,
168
174
  saveAutoRunConfig,
175
+ // 工具管理
176
+ allTools,
177
+ enabledTools,
178
+ saveEnabledTools,
169
179
  } = useChat({
170
180
  adapter,
171
181
  defaultModel,
@@ -173,6 +183,10 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
173
183
  onToolComplete,
174
184
  })
175
185
 
186
+ const handleCancelToolCall = useCallback((_toolCallId: string) => {
187
+ cancelRequest()
188
+ }, [cancelRequest])
189
+
176
190
  // 暴露给外部的方法
177
191
  useImperativeHandle(ref, () => ({
178
192
  setInputText: (text: string) => {
@@ -213,16 +227,44 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
213
227
  return scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD
214
228
  }, [])
215
229
 
216
- // 处理滚动事件
230
+ // 处理滚动事件(优化:检测滚动方向,区分程序化滚动)
217
231
  const handleScroll = useCallback(() => {
218
- setShouldAutoScroll(isNearBottom())
232
+ if (!messagesRef.current) return
233
+
234
+ // 忽略程序化滚动触发的事件
235
+ if (isProgrammaticScrollRef.current) return
236
+
237
+ const { scrollTop } = messagesRef.current
238
+ const isScrollingUp = scrollTop < lastScrollTopRef.current
239
+ lastScrollTopRef.current = scrollTop
240
+
241
+ // 用户向上滚动时,立即停止自动滚动
242
+ if (isScrollingUp) {
243
+ setShouldAutoScroll(false)
244
+ return
245
+ }
246
+
247
+ // 用户向下滚动到底部附近时,恢复自动滚动
248
+ if (isNearBottom()) {
249
+ setShouldAutoScroll(true)
250
+ }
219
251
  }, [isNearBottom])
220
252
 
221
253
  // 滚动到底部
222
254
  const scrollToBottom = useCallback((force = false) => {
223
255
  if (messagesRef.current && (force || shouldAutoScroll)) {
256
+ // 标记为程序化滚动,避免触发 handleScroll 逻辑
257
+ isProgrammaticScrollRef.current = true
224
258
  messagesRef.current.scrollTop = messagesRef.current.scrollHeight
225
- setShouldAutoScroll(true)
259
+ lastScrollTopRef.current = messagesRef.current.scrollTop
260
+ // 强制滚动时才恢复自动滚动
261
+ if (force) {
262
+ setShouldAutoScroll(true)
263
+ }
264
+ // 延迟重置标志,确保 scroll 事件已处理
265
+ requestAnimationFrame(() => {
266
+ isProgrammaticScrollRef.current = false
267
+ })
226
268
  }
227
269
  }, [shouldAutoScroll])
228
270
 
@@ -242,8 +284,10 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
242
284
  }, [isLoading, scrollToBottom])
243
285
 
244
286
  // 发送消息
245
- const handleSend = useCallback((text: string) => {
246
- sendMessage(text)
287
+ const handleSend = useCallback((text: string, images?: ImageData[]) => {
288
+ // 将 ImageData[] 转换为 string[] (data URL)
289
+ const imageUrls = images?.map(img => `data:${img.mimeType};base64,${img.base64}`)
290
+ sendMessage(text, imageUrls)
247
291
  }, [sendMessage])
248
292
 
249
293
  // @ 上下文
@@ -262,14 +306,20 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
262
306
 
263
307
  // 重新发送(编辑后)
264
308
  const handleResend = useCallback(
265
- (_index: number, text: string) => {
266
- // 删除当前消息及后续消息,然后重新发送
267
- // 这里简化处理,实际需要更完善的逻辑
268
- sendMessage(text)
309
+ (index: number, text: string) => {
310
+ // 与 Vue 版一致:使用分叉重发(以当前索引为锚点,删除后续并继续生成)
311
+ resendFromIndex(index, text)
269
312
  },
270
- [sendMessage]
313
+ [resendFromIndex]
271
314
  )
272
315
 
316
+ // 创建新会话(重置输入框状态)
317
+ const handleNewSession = useCallback(async () => {
318
+ await createNewSession()
319
+ // 重置输入框状态
320
+ inputRef.current?.clear()
321
+ }, [createNewSession])
322
+
273
323
  // 关闭
274
324
  const handleClose = useCallback(() => {
275
325
  onClose?.()
@@ -290,6 +340,15 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
290
340
  }
291
341
  }, [saveAutoRunConfig, showToast])
292
342
 
343
+ const handleUpdateEnabledTools = useCallback(async (tools: string[] | undefined) => {
344
+ try {
345
+ await saveEnabledTools(tools)
346
+ } catch (error) {
347
+ console.error('保存工具开关失败:', error)
348
+ showToast('保存工具开关失败', 'error')
349
+ }
350
+ }, [saveEnabledTools, showToast])
351
+
293
352
  // 模式变更现在在 ToolCallPart 中处理,不再需要此函数
294
353
 
295
354
  // 清空所有对话
@@ -369,7 +428,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
369
428
 
370
429
  return (
371
430
  <ChatInputProvider value={inputContextValue}>
372
- <RenderersProvider blockRenderers={{}} toolRenderers={toolRenderers}>
431
+ <PartRenderersProvider partRenderers={partRenderers}>
373
432
  <div className={`chat-panel ${className}`.trim()}>
374
433
  {/* 确认弹窗 */}
375
434
  <ConfirmDialog
@@ -399,6 +458,9 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
399
458
  <SettingsPanel
400
459
  visible={settingsPanelVisible}
401
460
  config={autoRunConfig}
461
+ allTools={allTools}
462
+ enabledTools={enabledTools}
463
+ onUpdateEnabledTools={handleUpdateEnabledTools}
402
464
  onChange={handleSaveSettings}
403
465
  onClose={() => setSettingsPanelVisible(false)}
404
466
  />
@@ -409,7 +471,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
409
471
  sessions={sessions}
410
472
  currentSessionId={currentSessionId}
411
473
  showClose={!!onClose}
412
- onNewSession={createNewSession}
474
+ onNewSession={handleNewSession}
413
475
  onSwitchSession={switchSession}
414
476
  onDeleteSession={deleteSession}
415
477
  onHideSession={hideSession}
@@ -442,6 +504,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
442
504
  timestamp={msg.timestamp}
443
505
  stepsExpandedType={stepsExpandedType}
444
506
  adapter={adapter}
507
+ onCancelToolCall={handleCancelToolCall}
445
508
  autoRunConfig={autoRunConfig}
446
509
  onSaveConfig={saveAutoRunConfig}
447
510
  onCopy={() => copyMessage(msg.id)}
@@ -481,7 +544,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(({
481
544
  onAtContext={handleAtContext}
482
545
  />
483
546
  </div>
484
- </RenderersProvider>
547
+ </PartRenderersProvider>
485
548
  </ChatInputProvider>
486
549
  )
487
550
  })
@@ -254,3 +254,84 @@
254
254
  .toggle-switch input:focus + .toggle-slider {
255
255
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
256
256
  }
257
+
258
+ /* 工具管理 */
259
+ .setting-section-header {
260
+ display: flex;
261
+ align-items: flex-start;
262
+ justify-content: space-between;
263
+ gap: 16px;
264
+ padding: 16px 0 8px;
265
+ border-bottom: 1px solid var(--chat-border, #333);
266
+ }
267
+
268
+ .disable-all-btn {
269
+ flex-shrink: 0;
270
+ height: 32px;
271
+ padding: 0 12px;
272
+ border-radius: 8px;
273
+ border: 1px solid var(--chat-border, #333);
274
+ background: rgba(255, 255, 255, 0.06);
275
+ color: var(--chat-text, #fff);
276
+ font-size: 13px;
277
+ cursor: pointer;
278
+ transition: all 0.15s ease;
279
+ }
280
+
281
+ .disable-all-btn:hover {
282
+ background: rgba(255, 255, 255, 0.1);
283
+ }
284
+
285
+ .disable-all-btn:disabled {
286
+ opacity: 0.5;
287
+ cursor: not-allowed;
288
+ }
289
+
290
+ .tools-list {
291
+ display: flex;
292
+ flex-direction: column;
293
+ }
294
+
295
+ .tool-item {
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: space-between;
299
+ gap: 16px;
300
+ padding: 14px 0;
301
+ border-bottom: 1px solid var(--chat-border, #333);
302
+ }
303
+
304
+ .tool-item:last-child {
305
+ border-bottom: none;
306
+ }
307
+
308
+ .tool-info {
309
+ flex: 1;
310
+ min-width: 0;
311
+ }
312
+
313
+ .tool-name {
314
+ font-size: 14px;
315
+ font-weight: 500;
316
+ color: var(--chat-text, #fff);
317
+ margin-bottom: 4px;
318
+ word-break: break-all;
319
+ }
320
+
321
+ .tool-description {
322
+ font-size: 13px;
323
+ color: var(--chat-text-muted, #888);
324
+ line-height: 1.5;
325
+ word-break: break-word;
326
+ }
327
+
328
+ .no-tools {
329
+ padding: 16px 0;
330
+ color: var(--chat-text, #fff);
331
+ }
332
+
333
+ .no-tools-hint {
334
+ font-size: 12px;
335
+ color: var(--chat-text-muted, #888);
336
+ margin-top: 8px;
337
+ }
@@ -14,6 +14,12 @@ import './SettingsPanel.css'
14
14
  interface SettingsPanelProps {
15
15
  visible: boolean
16
16
  config: AutoRunConfig
17
+ /** 所有可用工具列表(用于工具管理) */
18
+ allTools?: Array<{ name: string; description: string }>
19
+ /** 启用的工具名称列表(undefined 表示全部启用) */
20
+ enabledTools?: string[] | undefined
21
+ /** 更新工具开关(持久化由上层处理) */
22
+ onUpdateEnabledTools?: (tools: string[] | undefined) => void
17
23
  onClose: () => void
18
24
  onChange: (config: AutoRunConfig) => void
19
25
  }
@@ -23,7 +29,7 @@ const sections = [
23
29
  { id: 'indexing', label: '索引与文档', icon: 'lucide:database' },
24
30
  ] as const
25
31
 
26
- export function SettingsPanel({ visible, config, onClose, onChange }: SettingsPanelProps) {
32
+ export function SettingsPanel({ visible, config, allTools, enabledTools, onUpdateEnabledTools, onClose, onChange }: SettingsPanelProps) {
27
33
  const [currentSection, setCurrentSection] = useState<string>('agent')
28
34
 
29
35
  const currentSectionLabel = useMemo(
@@ -54,6 +60,54 @@ export function SettingsPanel({ visible, config, onClose, onChange }: SettingsPa
54
60
  }
55
61
  }, [onClose])
56
62
 
63
+ const allToolNames = useMemo(() => (allTools ?? []).map((t) => t.name), [allTools])
64
+
65
+ /** 判断工具是否启用 */
66
+ const isToolEnabled = useCallback((toolName: string): boolean => {
67
+ // undefined 表示全部启用
68
+ if (enabledTools === undefined) return true
69
+ return enabledTools.includes(toolName)
70
+ }, [enabledTools])
71
+
72
+ /** 处理工具开关切换 */
73
+ const handleToolToggle = useCallback((toolName: string, checked: boolean) => {
74
+ if (!onUpdateEnabledTools) return
75
+
76
+ // undefined = 全部启用
77
+ if (enabledTools === undefined) {
78
+ if (checked) return
79
+ onUpdateEnabledTools(allToolNames.filter((n) => n !== toolName))
80
+ return
81
+ }
82
+
83
+ const set = new Set(enabledTools)
84
+ if (checked) set.add(toolName)
85
+ else set.delete(toolName)
86
+
87
+ // 如果全部启用,回到 undefined(更符合默认语义)
88
+ if (allToolNames.length > 0 && set.size === allToolNames.length) {
89
+ onUpdateEnabledTools(undefined)
90
+ return
91
+ }
92
+
93
+ onUpdateEnabledTools(Array.from(set))
94
+ }, [enabledTools, allToolNames, onUpdateEnabledTools])
95
+
96
+ /** 一键禁用所有工具 */
97
+ const handleDisableAllTools = useCallback(() => {
98
+ onUpdateEnabledTools?.([])
99
+ }, [onUpdateEnabledTools])
100
+
101
+ const isAllToolsDisabled = useMemo(
102
+ () => enabledTools !== undefined && enabledTools.length === 0,
103
+ [enabledTools]
104
+ )
105
+
106
+ const handleEnableAllTools = useCallback(() => {
107
+ // 恢复默认:undefined 表示全部启用
108
+ onUpdateEnabledTools?.(undefined)
109
+ }, [onUpdateEnabledTools])
110
+
57
111
  if (!visible) return null
58
112
 
59
113
  return (
@@ -105,6 +159,47 @@ export function SettingsPanel({ visible, config, onClose, onChange }: SettingsPa
105
159
  />
106
160
  </div>
107
161
  </div>
162
+
163
+ {/* 工具管理 */}
164
+ <div className="setting-section">
165
+ <div className="setting-section-header">
166
+ <div className="setting-info">
167
+ <div className="setting-label">工具管理</div>
168
+ <div className="setting-description">控制哪些工具可以被 AI 使用</div>
169
+ </div>
170
+ <button
171
+ className="disable-all-btn"
172
+ onClick={isAllToolsDisabled ? handleEnableAllTools : handleDisableAllTools}
173
+ disabled={!allTools?.length}
174
+ >
175
+ {isAllToolsDisabled ? '启用所有工具' : '禁用所有工具'}
176
+ </button>
177
+ </div>
178
+
179
+ <div className="tools-list">
180
+ {allTools && allTools.length > 0 ? (
181
+ allTools.map((tool) => (
182
+ <div key={tool.name} className="tool-item">
183
+ <div className="tool-info">
184
+ <div className="tool-name">{tool.name}</div>
185
+ <div className="tool-description">{tool.description}</div>
186
+ </div>
187
+ <ToggleSwitch
188
+ checked={isToolEnabled(tool.name)}
189
+ onChange={(checked) => handleToolToggle(tool.name, checked)}
190
+ />
191
+ </div>
192
+ ))
193
+ ) : (
194
+ <div className="no-tools">
195
+ <div>暂无可用工具</div>
196
+ <div className="no-tools-hint">
197
+ 工具需要在创建 Electron Bridge 时通过 <code>tools</code> 参数注入
198
+ </div>
199
+ </div>
200
+ )}
201
+ </div>
202
+ </div>
108
203
  </>
109
204
  )}
110
205
 
@@ -8,7 +8,6 @@
8
8
 
9
9
  .chat-input.message-variant {
10
10
  padding: 0;
11
- margin-bottom: 16px;
12
11
  }
13
12
 
14
13
  .input-container {