@xinghunm/ai-chat 0.1.0

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 ADDED
@@ -0,0 +1,263 @@
1
+ # @xinghunm/ai-chat
2
+
3
+ 提供完整 AI 对话 UI 的 React 组件库,支持会话管理、流式响应、Agent 模式和图片附件。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @xinghunm/ai-chat
9
+ # 或
10
+ pnpm add @xinghunm/ai-chat
11
+ ```
12
+
13
+ ### Peer Dependencies
14
+
15
+ 需在项目中单独安装:
16
+
17
+ ```bash
18
+ npm install @emotion/react @emotion/styled @xinghunm/compass-ui axios react react-dom react-markdown rehype-katex remark-gfm remark-math zustand
19
+ ```
20
+
21
+ ## 快速开始
22
+
23
+ 推荐通过 `transport` 接入,这样 `ai-chat` 只负责通用 UI,后端协议由接入层决定:
24
+
25
+ ```tsx
26
+ import { AiChat, createDefaultChatTransport } from '@xinghunm/ai-chat'
27
+
28
+ const transport = createDefaultChatTransport({
29
+ apiBaseUrl: '/ai-api',
30
+ authToken: 'Bearer your-token-here',
31
+ })
32
+
33
+ export const App = () => <AiChat transport={transport} />
34
+ ```
35
+
36
+ 显示会话列表侧边栏:
37
+
38
+ ```tsx
39
+ <AiChat transport={transport} showConversationList />
40
+ ```
41
+
42
+ 如果你还在使用内置默认协议,也可以继续传 `apiBaseUrl` 和 `authToken`。`AiChatProvider` 会在内部自动创建默认 transport,属于兼容模式。
43
+
44
+ 如果只是路径不同,也不需要自己重写整个 transport,可以只覆盖默认 adapter 的 endpoints:
45
+
46
+ ```tsx
47
+ const transport = createDefaultChatTransport({
48
+ apiBaseUrl: '/ai-api',
49
+ authToken: 'Bearer your-token-here',
50
+ endpoints: {
51
+ models: '/catalog/models',
52
+ completions: '/chat/run',
53
+ terminate: '/chat/stop',
54
+ },
55
+ })
56
+ ```
57
+
58
+ ## 完整用法
59
+
60
+ 如需最大灵活性,可在 `AiChatProvider` 内手动组合子组件:
61
+
62
+ ```tsx
63
+ import { AiChatProvider, ChatConversationList, ChatThread, ChatComposer } from '@xinghunm/ai-chat'
64
+ import type { ChatTransport } from '@xinghunm/ai-chat'
65
+
66
+ const transport: ChatTransport = {
67
+ async getModels() {
68
+ return {
69
+ data: [{ id: 'my-model', object: 'model' }],
70
+ }
71
+ },
72
+ async startStream({
73
+ sessionId,
74
+ model,
75
+ mode,
76
+ content,
77
+ onSessionId,
78
+ onUpdate,
79
+ onDone,
80
+ onError,
81
+ signal,
82
+ }) {
83
+ try {
84
+ const response = await fetch('/custom-chat/stream', {
85
+ method: 'POST',
86
+ signal,
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ sessionId, model, mode, content }),
89
+ })
90
+ const data = await response.json()
91
+ onSessionId?.(data.sessionId)
92
+ onUpdate({ content: data.answer })
93
+ onDone?.()
94
+ } catch (error) {
95
+ onError?.(error instanceof Error ? error : new Error('Unknown stream error'))
96
+ }
97
+ },
98
+ async terminateStream(sessionId) {
99
+ await fetch('/custom-chat/terminate', {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ sessionId }),
103
+ })
104
+ return { terminated: true }
105
+ },
106
+ }
107
+
108
+ export const CustomChat = () => (
109
+ <AiChatProvider
110
+ transport={transport}
111
+ defaultMode="agent"
112
+ labels={{
113
+ sendButton: '发送',
114
+ placeholder: '输入你的问题…',
115
+ newChat: '新建对话',
116
+ }}
117
+ >
118
+ <div style={{ display: 'flex', height: '100vh' }}>
119
+ <ChatConversationList />
120
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
121
+ <ChatThread />
122
+ <ChatComposer />
123
+ </div>
124
+ </div>
125
+ </AiChatProvider>
126
+ )
127
+ ```
128
+
129
+ ## Props API
130
+
131
+ ### `AiChatProps`
132
+
133
+ 一体化 `AiChat` 组件的 Props,继承全部 `AiChatProviderProps`(不含 `children`)。
134
+
135
+ | 属性 | 类型 | 默认值 | 说明 |
136
+ | ------------------------ | --------------- | --------- | ----------------------------------------------------------------------------------------------- |
137
+ | `transport` | `ChatTransport` | — | 推荐。由接入方提供的传输适配器。 |
138
+ | `apiBaseUrl` | `string` | — | 兼容模式。创建默认内置 transport。 |
139
+ | `authToken` | `string` | — | 兼容模式下默认 transport 使用的鉴权头。 |
140
+ | `defaultMode` | `ChatAgentMode` | `"agent"` | 新会话的初始 Agent 模式。 |
141
+ | `labels` | `AiChatLabels` | — | 可选的 UI 文案覆盖。 |
142
+ | `showConversationList` | `boolean` | `false` | 为 `true` 时渲染会话列表侧边栏。 |
143
+ | `enableImageAttachments` | `boolean` | `true` | 为 `false` 时隐藏上传按钮、禁用粘贴图片,并使程序化调用 `pickImages`/`pasteImages` 变为 no-op。 |
144
+
145
+ ### `AiChatProviderProps`
146
+
147
+ `AiChatProvider` 上下文提供者组件的 Props。
148
+
149
+ `AiChatProvider` 有两种接入方式,二选一:
150
+
151
+ | 属性 | 类型 | 必填 | 说明 |
152
+ | ------------------------ | -------------------------- | ---- | ------------------------------------------------------------------------------------------------------------ |
153
+ | `transport` | `ChatTransport` | 是 | 推荐。完全自定义的传输层。 |
154
+ | `defaultMode` | `ChatAgentMode` | 否 | 新会话的初始 Agent 模式。 |
155
+ | `labels` | `AiChatLabels` | 否 | 可选的 UI 文案覆盖。 |
156
+ | `renderMessageBlock` | `ChatMessageBlockRenderer` | 否 | 自定义消息 block 渲染器,用于承接 `type: "custom"` 等扩展。 |
157
+ | `enableImageAttachments` | `boolean` | 否 | 为 `false` 时隐藏上传按钮、禁用粘贴图片,并使程序化调用 `pickImages`/`pasteImages` 变为 no-op。默认 `true`。 |
158
+ | `children` | `ReactNode` | 是 | Provider 内部渲染的子元素。 |
159
+
160
+ 兼容模式:
161
+
162
+ | 属性 | 类型 | 必填 | 说明 |
163
+ | ------------------------ | --------------------------- | ---- | ------------------------------------------------------------------------------------------------------------ |
164
+ | `apiBaseUrl` | `string` | 是 | 默认 adapter 的基础 URL。 |
165
+ | `authToken` | `string` | 是 | 默认 adapter 使用的 Authorization 请求头值。 |
166
+ | `transformStreamPacket` | `TransformChatStreamPacket` | 否 | 默认 adapter 的流式包归一化扩展点。 |
167
+ | `defaultMode` | `ChatAgentMode` | 否 | 新会话的初始 Agent 模式。 |
168
+ | `labels` | `AiChatLabels` | 否 | 可选的 UI 文案覆盖。 |
169
+ | `renderMessageBlock` | `ChatMessageBlockRenderer` | 否 | 自定义消息 block 渲染器,用于承接 `type: "custom"` 等扩展。 |
170
+ | `enableImageAttachments` | `boolean` | 否 | 为 `false` 时隐藏上传按钮、禁用粘贴图片,并使程序化调用 `pickImages`/`pasteImages` 变为 no-op。默认 `true`。 |
171
+ | `children` | `ReactNode` | 是 | Provider 内部渲染的子元素。 |
172
+
173
+ ### `ChatTransport`
174
+
175
+ `ChatTransport` 是通用化后的核心扩展点:
176
+
177
+ | 方法 | 说明 |
178
+ | ------------------- | -------------------------------------------------------- |
179
+ | `getModels()` | 返回模型列表,用于填充模型选择器。 |
180
+ | `startStream()` | 发送用户消息并通过 `onUpdate` 推送归一化后的流式 patch。 |
181
+ | `terminateStream()` | 请求终止当前会话的流式响应。 |
182
+
183
+ 库内同时导出了 `createDefaultChatTransport()`,用于快速复用当前 `/models`、`/chat/completions`、`/chat/terminate` 协议。
184
+
185
+ `createDefaultChatTransport()` 同时支持 `endpoints` 覆盖:
186
+
187
+ | 键 | 默认值 | 说明 |
188
+ | ------------- | --------------------- | ---------------------- |
189
+ | `models` | `"/models"` | 模型列表接口路径。 |
190
+ | `completions` | `"/chat/completions"` | 流式聊天接口路径。 |
191
+ | `terminate` | `"/chat/terminate"` | 停止流式响应接口路径。 |
192
+
193
+ ### `AiChatLabels`
194
+
195
+ 所有字段均为可选,未指定的字段回退到 `DEFAULT_AI_CHAT_LABELS` 中的英文默认值。
196
+
197
+ | 键 | 默认值 | 说明 |
198
+ | ----------------------- | ------------------------------ | ------------------------------ |
199
+ | `sendButton` | `"Send"` | 发送按钮文案。 |
200
+ | `stopButton` | `"Stop"` | 停止/中止按钮文案。 |
201
+ | `placeholder` | `"Ask something..."` | 输入框占位文本。 |
202
+ | `modeLabelAsk` | `"Ask"` | Ask 模式标签。 |
203
+ | `modeLabelPlan` | `"Plan"` | Plan 模式标签。 |
204
+ | `modeLabelAgent` | `"Agent"` | Agent 模式标签。 |
205
+ | `newChat` | `"New Chat"` | 新建对话按钮文案。 |
206
+ | `emptyStateTitle` | `"How can I help you?"` | 空消息状态的主标题。 |
207
+ | `emptyStateSubtitle` | `"Start a conversation"` | 空消息状态的副标题。 |
208
+ | `attachmentLimitNotice` | `"Images exceeded the limit…"` | 达到附件数量上限时的提示文案。 |
209
+
210
+ ## Store
211
+
212
+ 在高级场景下(如读取流式状态、以编程方式切换会话),可在 `AiChatProvider` 的子孙组件内通过内置 hooks 访问底层 Zustand store:
213
+
214
+ ```tsx
215
+ import { useChatStore, useChatContext } from '@xinghunm/ai-chat'
216
+
217
+ // 选取 store 的某个切片(仅在该切片变化时触发重渲染)
218
+ const activeSessionId = useChatStore((s) => s.activeSessionId)
219
+
220
+ // 获取完整上下文,包括 transport 和合并后的 labels
221
+ const { labels, transport } = useChatContext()
222
+ ```
223
+
224
+ 两个 hooks 在 `AiChatProvider` 外部调用时均会抛出异常。
225
+
226
+ ## 扩展自定义 Block
227
+
228
+ `ai-chat` 现在支持把“通用聊天壳子”和“业务工作流卡片”解耦。推荐做法是:
229
+
230
+ 1. 用 `transformStreamPacket` 把后端的自定义 packet 转成 `blocks`
231
+ 2. 用 `renderMessageBlock` 渲染 `type: "custom"` 的 block
232
+
233
+ ```tsx
234
+ import type { ChatMessageBlockRendererProps, TransformChatStreamPacket } from '@xinghunm/ai-chat'
235
+
236
+ const transformStreamPacket: TransformChatStreamPacket = ({ packet, defaultUpdate }) => {
237
+ if (
238
+ packet.type === 'message_complete' &&
239
+ typeof packet.data === 'object' &&
240
+ packet.data !== null &&
241
+ 'widget' in packet.data
242
+ ) {
243
+ return {
244
+ ...defaultUpdate,
245
+ blocks: [
246
+ { type: 'custom', kind: 'widget', data: (packet.data as { widget: unknown }).widget },
247
+ ],
248
+ }
249
+ }
250
+
251
+ return defaultUpdate
252
+ }
253
+
254
+ const renderMessageBlock = ({ block }: ChatMessageBlockRendererProps) => {
255
+ if (block.type !== 'custom' || block.kind !== 'widget') {
256
+ return null
257
+ }
258
+
259
+ return <pre>{JSON.stringify(block.data, null, 2)}</pre>
260
+ }
261
+ ```
262
+
263
+ 这样业务卡片可以放在接入层或扩展包里,基础包继续只负责通用聊天体验。