@blade-hq/agent-react 2608.0.7 → 2608.0.8-beta.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 CHANGED
@@ -68,6 +68,66 @@ await session?.send("你好")
68
68
  - 自动创建只发生一次(含 React StrictMode 双跑);创建出的 id 通过 `onSessionCreated` 交还,重挂载时把它作为 `sessionId` 传回即可复用会话。
69
69
  - 同一 `sessionId` 的多次调用复用同一 `AgentSession` 实例(引用计数);卸载后延迟释放,路由抖动不会断连。
70
70
 
71
+ ## useReplay
72
+
73
+ 会话回放(演示 / 彩排):拿一个已经聊完的会话当素材,重现当时的回复和工具调用,
74
+ **完全不调用模型**。
75
+
76
+ **进入回放是"派生一个新会话",不是把当前会话切模式**:`startReplay()` 返回的是新会话
77
+ 的 id,拿它去渲染(`<ChatView sessionId={newId} />` 或自己 `connect`)才进入回放。源会话
78
+ 原样不动。连上之后也不会自动播——用户照着源会话的问题问,才逐句重现。
79
+
80
+ 同一个 hook 覆盖两种身份——传进去的会话既可能是准备被回放的**源会话**,也可能是**回放中**的会话:
81
+
82
+ ```tsx
83
+ // —— 源会话页面:提供"开始回放"入口 ——
84
+ const { session: sourceSession } = useAgentSession(sourceId)
85
+ const source = useReplay(sourceSession)
86
+
87
+ source.canReplay // false 时 source.unsupportedReason 可直接展示
88
+ const replayId = await source.startReplay(5) // 返回**新建的**回放会话 id,路由过去
89
+
90
+ // —— 回放会话页面:状态与控制 ——
91
+ const { session: replaySession } = useAgentSession(replayId)
92
+ const replay = useReplay(replaySession)
93
+
94
+ replay.isReplay // 是不是在回放
95
+ replay.speed // 当前倍速
96
+ replay.canControl // 改回放是 owner 专属;分享出去的只读会话为 false
97
+ replay.mismatch // 输入和录制对不上时的待决策项
98
+ await replay.setSpeed(2)
99
+ await replay.exitToAutonomous() // 退出回放,之后真的运行
100
+ ```
101
+
102
+ 配套两个组件,直接接上 `useReplay()` 的返回值即可:
103
+
104
+ ```tsx
105
+ <ReplayBar
106
+ isReplay={replay.isReplay}
107
+ speed={replay.speed}
108
+ canControl={replay.canControl}
109
+ onSpeedChange={(s) => void replay.setSpeed(s)}
110
+ onExit={() => void replay.exitToAutonomous()}
111
+ />
112
+ <ReplayMismatchPrompt mismatch={replay.mismatch} />
113
+ ```
114
+
115
+ 返回值见 `UseReplayResult`,冲突项见 `ReplayMismatch`,组件属性见 `ReplayBarProps` /
116
+ `ReplayMismatchPromptProps`。
117
+
118
+ 只读身份(分享链接打开的会话)仍然会显示「回放模式」——这是会话事实,藏起来会让分享出去的
119
+ 回放看着像真实运行;但倍速和退出会被禁用,因为服务端的 `PATCH` 是 owner 专属。
120
+
121
+ **`ChatView` 已经自带这一整套**——用它打开回放会话,状态条、倍速、退出、冲突卡片都在,
122
+ 不用自己接。下面这些 API 是给自建 UI 用的。
123
+
124
+ **注册即接管**:底层的 `replayMismatch` 只交给最先注册的处理器。调用 `useReplay()`(或用
125
+ `ChatView`)就意味着由它接管冲突;两者都不用则维持 SDK 默认行为——静默转为真实运行。
126
+
127
+ 回放状态本身住在 `SessionState.replay` 里,本 hook 只是它的 React 绑定。非 React 宿主
128
+ (Vue、Svelte、自建 UI)直接订阅快照即可,不需要这个包,用法见
129
+ [agent-client 的会话回放章节](../agent-client/README.md#会话回放演示--彩排)。
130
+
71
131
  ## ChatView
72
132
 
73
133
  完整聊天界面:消息列表(Markdown、代码高亮、工具调用、提问卡片)+ 输入框 + 连接状态条 + 登录引导。
@@ -96,6 +156,95 @@ await session?.send("你好")
96
156
  - `renderers.toolCall` 返回 `null` 时回落到默认工具卡片。
97
157
  - **内边距按容器宽度伸缩**,嵌进窄侧栏(300–400px)时会自动收窄,不看浏览器视口。代价是聊天容器需要有确定宽度——放在 flex/grid 里被拉伸,或显式给宽度都可以;放进 `width: fit-content`、`inline-flex` 这类由内容决定宽度的父容器会塌缩。
98
158
 
159
+ ## 纯 LLM 模式(不建会话)
160
+
161
+ 有些应用只要「一问一答」——分类、改写、总结,不需要智能体、不需要会话、也不需要用户登录
162
+ Blade。这时把 `ChatView` 切到 `mode="llm"`,或者直接用下层的 `LlmChat`,界面完全一样:
163
+
164
+ ```tsx
165
+ <ChatView /> // 默认:智能体模式,行为不变
166
+ <ChatView mode="llm" llm={{ baseURL: "/api/llm", model }} /> // 切成纯 LLM
167
+ <LlmChat baseURL="/api/llm" model={model} /> // 或者直接用(LlmChatProps)
168
+ <AgentChat sessionId={id} /> // 智能体那侧同样可以直接用(AgentChatProps)
169
+ ```
170
+
171
+ **协议就是 OpenAI 的**:`POST {baseURL}/chat/completions`,`stream: true`,读
172
+ `choices[0].delta`。任何 OpenAI 兼容实现(Blade 网关、vLLM、Ollama、OpenAI 本身、你自己的
173
+ 透传代理)都能直接接,不用为这个组件专门写后端。
174
+
175
+ **密钥默认不进浏览器**:把 `baseURL` 指向自己后端的一个路径,后端补上 `Authorization` 再原样
176
+ 转发即可(协议是标准的,这个代理就是纯转发)。确实要前端直连时才传 `apiKey`——只适合受控内网、
177
+ 演示,或密钥本身就属于当前用户的场景。
178
+
179
+ **模型服务地址和模型名别写死**,用 `client.models.list()` 拿,各部署端口不一样:
180
+
181
+ ```tsx
182
+ const { baseUrl, defaultServiceModel, models } = await client.models.list()
183
+
184
+ // 后端透传(推荐):baseURL 指向自己的路径,模型名仍然要用 defaultServiceModel
185
+ <ChatView mode="llm" llm={{ baseURL: "/api/llm", model: defaultServiceModel }} />
186
+
187
+ // 浏览器直连:仅当 baseUrl 对浏览器也可达时
188
+ <ChatView mode="llm" llm={{ baseURL: baseUrl, model: defaultServiceModel, apiKey }} />
189
+
190
+ // 让用户选模型时,过滤掉不在这个服务上的
191
+ models.filter((m) => m.serviceModelId)
192
+ ```
193
+
194
+ **模型名要用 `defaultServiceModel` / `serviceModelId`,不是 `default` / `id`。** 后者是平台内部
195
+ 标识,平台接了多个模型服务时形如 `provider-xxx::deepseek-v4-flash`,发给模型服务只会得到
196
+ 「模型不存在」。`baseUrl` 同理是服务端视角的地址,平台常配成 `http://127.0.0.1:30000/v1`,
197
+ 那是给自己后端转发用的,不能直接丢给浏览器。
198
+
199
+ **工具调用走 OpenAI 标准的多轮流程**,不需要任何私有扩展:声明 `tools`,在 `onToolCall`
200
+ (收到 `LlmToolCall`)里执行——通常就是调你自己应用的同源接口——返回值会作为
201
+ `role: "tool"` 消息回喂给模型,直到它给出最终回答或达到 `maxToolRounds`(默认 4)。
202
+ 每一轮工具调用都会渲染成和智能体模式一样的工具卡片。
203
+
204
+ ```tsx
205
+ <LlmChat
206
+ baseURL="/api/llm"
207
+ model={model}
208
+ system="你是订单助手"
209
+ tools={[{ type: "function", function: { name: "query_orders", parameters: schema } }]} // ChatCompletionTool
210
+ onToolCall={async ({ name, arguments: args }) => {
211
+ if (name !== "query_orders") throw new Error(`没有这个函数:${name}`)
212
+ // 参数是模型生成的不可信输入:先校验类型,再用 URLSearchParams 拼,
213
+ // 直接塞进查询串的话,一个 `李航&limit=all` 就能多带出参数
214
+ const { owner } = JSON.parse(args || "{}")
215
+ if (typeof owner !== "string") throw new Error("owner 必须是字符串")
216
+ const query = new URLSearchParams({ owner })
217
+ const resp = await fetch(`/api/orders?${query}`)
218
+ if (!resp.ok) throw new Error(`订单接口返回 ${resp.status}`)
219
+ return resp.json()
220
+ }}
221
+ onReady={(handle) => setChat(handle)} // LlmChatHandle:insertText / send / reset
222
+ />
223
+ ```
224
+
225
+ ### 让用户自己改模型服务地址
226
+
227
+ 传 `advanced` 就在输入框上方多一行「高级设置」(默认收起),用户可以改模型服务地址和模型名;
228
+ 改过之后会显示「已自定义」,也能一键恢复默认。不传就一个节点都不渲染。
229
+
230
+ ```tsx
231
+ <LlmChat baseURL="/api/llm" model={model} advanced />
232
+ <LlmChat baseURL="/api/llm" model={model} advanced={{ apiKey: true, storage: "memory" }} />
233
+ <ChatView mode="llm" llm={{ baseURL: "/api/llm", model, advanced: true }} />
234
+ ```
235
+
236
+ `LlmAdvancedSettings` 可细调:`baseURL` / `model` 各自能不能改(默认都能)、`apiKey` 要不要
237
+ 出现(**默认不出现**,打开意味着密钥存在浏览器里,只适合受控场景)、`storage` 是记住选择
238
+ (`local`,默认)还是只在本次有效(`memory`)、`storageKey` 自定义存储键。用户改了会回调
239
+ `onOverrideChange`(`LlmOverride`),宿主想自己也记一份时用。
240
+
241
+ 其余可选项见 `LlmChatOptions`:`headers`、`historyTurns`(默认 12 轮)、`temperature`、
242
+ `extraBody`、`fetchImpl`。只要状态机、界面自己写的,用 `useLlmChat`(返回
243
+ `UseLlmChatResult`:`messages` / `isStreaming` / `error` / `send` / `stop` / `reset`)。
244
+
245
+ 纯 LLM 模式不做会话持久化、文件上传、提问卡片、记忆和检查点——那些是智能体协议的能力,
246
+ 需要就用默认模式。
247
+
99
248
  ## MarkdownContent
100
249
 
101
250
  单独渲染一段智能体消息 Markdown(含代码复制按钮、XSS 消毒;HTML 经 sanitize 白名单处理,不接受任意 raw HTML):
@@ -104,7 +253,7 @@ await session?.send("你好")
104
253
  <MarkdownContent content={markdownText} />
105
254
  ```
106
255
 
107
- 传了 `sessionId` 才会把智能体产出文件的 `MEDIA:` 行折叠成以文件名为文本的下载链接(下载要用它)。`ChatView` 内部已经传好,只有单独用 `MarkdownContent` 时需要自己给:
256
+ 传了 `sessionId` 才会把智能体产出文件的 `MEDIA:` 行折叠成下载卡片(下载要用它)。`ChatView` 内部已经传好,只有单独用 `MarkdownContent` 时需要自己给:
108
257
 
109
258
  ```tsx
110
259
  <MarkdownContent sessionId={sessionId}>{markdownText}</MarkdownContent>
@@ -153,4 +302,4 @@ blade-chat { --primary: 262 83% 58%; height: 640px; }
153
302
 
154
303
  为方便单包引入,本包 re-export 了 agent-client 的全部公开声明(`BladeClient`、`AgentSession`、`SessionState`、协议类型等),文档一律见 [agent-client 的 README](../agent-client/README.md)。完整签名见 [public-api.md](./public-api.md)。
155
304
 
156
- 本包自有声明:`BladeProvider`、`BladeProviderProps`、`useBladeClient`、`useAgentSession`、`UseAgentSessionOptions`、`UseAgentSessionResult`、`ChatView`、`ChatViewProps`、`ChatViewClassNames`、`ChatViewRenderers`、`ChatViewSlots`、`ToolCallRenderer`、`MarkdownContent`、`MarkdownContentProps`。
305
+ 本包自有声明:`BladeProvider`、`BladeProviderProps`、`useBladeClient`、`useAgentSession`、`UseAgentSessionOptions`、`UseAgentSessionResult`、`useReplay`、`UseReplayResult`、`ReplayMismatch`、`ReplayBar`、`ReplayBarProps`、`ReplayMismatchPrompt`、`ReplayMismatchPromptProps`、`ChatView`、`ChatViewProps`、`ChatViewClassNames`、`ChatViewRenderers`、`ChatViewSlots`、`ToolCallRenderer`、`MarkdownContent`、`MarkdownContentProps`。
@@ -0,0 +1,18 @@
1
+ import type { AgentSession, CreateSessionRequest } from "@blade-hq/agent-client";
2
+ import { type ChatPresentationProps } from "./ChatSurface";
3
+ export interface AgentChatProps extends ChatPresentationProps {
4
+ /** 连接既有会话;不传则按 createOptions 自动新建。 */
5
+ sessionId?: string;
6
+ createOptions?: CreateSessionRequest;
7
+ /** 自动创建出的 sessionId 通过此回调交还调用方持有。 */
8
+ onSessionCreated?: (sessionId: string) => void;
9
+ /** 会话连接完成后回调(页面协作场景拿 AgentSession 实例用)。 */
10
+ onSessionReady?: (session: AgentSession) => void;
11
+ /** 智能体下发给宿主页面的指令处理器(action → handler)。 */
12
+ commands?: Record<string, (data: unknown) => void>;
13
+ }
14
+ /**
15
+ * 智能体模式的聊天界面:连接(或新建)一个 Blade 会话,渲染消息流与输入框。
16
+ * 状态全部来自 useAgentSession,不依赖任何全局 store。
17
+ */
18
+ export declare function AgentChat(props: AgentChatProps): import("react/jsx-runtime").JSX.Element;
@@ -7,7 +7,7 @@ interface Props {
7
7
  onAnswer?: (answer: string, toolCallId: string, answerData: AskUserAnswerData) => void;
8
8
  sessionStatus?: string;
9
9
  toolCallRenderer?: ToolCallRenderer;
10
- /** 传给 Markdown 渲染,用于把 MEDIA 行折叠成文件下载链接。 */
10
+ /** 传给 Markdown 渲染,用于把 MEDIA 行折叠成文件下载卡片。 */
11
11
  sessionId?: string;
12
12
  }
13
13
  /**
@@ -0,0 +1,59 @@
1
+ import type { AskUserAnswerData, ChatMessage, ConnectionStatus } from "@blade-hq/agent-client";
2
+ import type { ReactNode } from "react";
3
+ import type { ToolCallRenderer } from "./ToolCallBlock";
4
+ export interface ChatViewClassNames {
5
+ root?: string;
6
+ banner?: string;
7
+ messageList?: string;
8
+ chatInput?: string;
9
+ }
10
+ export interface ChatViewRenderers {
11
+ /** 自定义工具调用渲染;返回 null 走默认渲染。 */
12
+ toolCall?: ToolCallRenderer;
13
+ }
14
+ export interface ChatViewSlots {
15
+ /** 消息列表上方的自定义区域。 */
16
+ header?: ReactNode;
17
+ /** 无消息时的空状态。 */
18
+ emptyState?: ReactNode;
19
+ /** 输入框下方的自定义区域。 */
20
+ footer?: ReactNode;
21
+ }
22
+ /** 两种模式共用的外观属性。 */
23
+ export interface ChatPresentationProps {
24
+ classNames?: ChatViewClassNames;
25
+ renderers?: ChatViewRenderers;
26
+ slots?: ChatViewSlots;
27
+ placeholder?: string;
28
+ /** 配色主题,默认浅色。宿主已经在祖先元素上写了 data-theme 时不用传。 */
29
+ theme?: "light" | "dark";
30
+ }
31
+ /** 主题落到聊天根节点上:深色变量挂在 [data-theme="dark"] 上,由它继承给整棵子树。 */
32
+ export declare function themeAttr(theme: ChatPresentationProps["theme"]): "dark" | undefined;
33
+ interface ChatSurfaceProps extends ChatPresentationProps {
34
+ connection: ConnectionStatus;
35
+ errorMessage: string | null;
36
+ messages: ChatMessage[];
37
+ isStreaming: boolean;
38
+ isStopping: boolean;
39
+ inputText: string;
40
+ onInputChange: (value: string) => void;
41
+ onSend: (text: string) => Promise<boolean>;
42
+ onStop: () => void;
43
+ sessionStatus?: string;
44
+ askAnswers?: Record<string, AskUserAnswerData>;
45
+ onAnswer?: (answer: string, toolCallId: string, answerData: AskUserAnswerData) => void;
46
+ sessionId?: string;
47
+ /** 输入框上方的内部区域(纯 LLM 模式的高级设置用)。不传时不渲染任何节点。 */
48
+ beforeInput?: ReactNode;
49
+ /** 连接状态条下方的横幅(回放模式条用)。不传时不渲染任何节点。 */
50
+ banner?: ReactNode;
51
+ }
52
+ /**
53
+ * 消息流 + 输入框:智能体模式与纯 LLM 模式共用同一套外观,差别只在谁喂数据。
54
+ *
55
+ * 这里的 DOM 结构受 tests/chat-view-compat.test.tsx 的快照保护——接入方拿
56
+ * .blade-chat-* 类名写了自己的样式,结构一动他们的页面就歪。
57
+ */
58
+ export declare function ChatSurface({ theme, classNames, renderers, slots, placeholder, connection, errorMessage, messages, isStreaming, isStopping, inputText, onInputChange, onSend, onStop, sessionStatus, askAnswers, onAnswer, sessionId, beforeInput, banner, }: ChatSurfaceProps): import("react/jsx-runtime").JSX.Element;
59
+ export {};
@@ -1,43 +1,23 @@
1
- import type { AgentSession, CreateSessionRequest } from "@blade-hq/agent-client";
2
- import { type ReactNode } from "react";
3
- import type { ToolCallRenderer } from "./ToolCallBlock";
4
- export interface ChatViewClassNames {
5
- root?: string;
6
- banner?: string;
7
- messageList?: string;
8
- chatInput?: string;
9
- }
10
- export interface ChatViewRenderers {
11
- /** 自定义工具调用渲染;返回 null 走默认渲染。 */
12
- toolCall?: ToolCallRenderer;
13
- }
14
- export interface ChatViewSlots {
15
- /** 消息列表上方的自定义区域。 */
16
- header?: ReactNode;
17
- /** 无消息时的空状态。 */
18
- emptyState?: ReactNode;
19
- /** 输入框下方的自定义区域。 */
20
- footer?: ReactNode;
21
- }
22
- export interface ChatViewProps {
23
- /** 连接既有会话;不传则按 createOptions 自动新建。 */
24
- sessionId?: string;
25
- createOptions?: CreateSessionRequest;
26
- /** 自动创建出的 sessionId 通过此回调交还调用方持有。 */
27
- onSessionCreated?: (sessionId: string) => void;
28
- /** 会话连接完成后回调(页面协作场景拿 AgentSession 实例用)。 */
29
- onSessionReady?: (session: AgentSession) => void;
30
- /** 智能体下发给宿主页面的指令处理器(action → handler)。 */
31
- commands?: Record<string, (data: unknown) => void>;
32
- classNames?: ChatViewClassNames;
33
- renderers?: ChatViewRenderers;
34
- slots?: ChatViewSlots;
35
- placeholder?: string;
36
- /** 配色主题,默认浅色。宿主已经在祖先元素上写了 data-theme 时不用传。 */
37
- theme?: "light" | "dark";
1
+ import { type AgentChatProps } from "./AgentChat";
2
+ import { type LlmChatHandle } from "./LlmChat";
3
+ import type { LlmAdvancedSettings, LlmOverride } from "./LlmAdvancedSettings";
4
+ import type { LlmChatOptions } from "../hooks/use-llm-chat";
5
+ export type { ChatViewClassNames, ChatViewRenderers, ChatViewSlots, } from "./ChatSurface";
6
+ export interface ChatViewProps extends AgentChatProps {
7
+ /** 挂哪个下层组件。默认 `agent`——不传就是接 Blade 智能体会话的原有行为。 */
8
+ mode?: "agent" | "llm";
9
+ /** `mode="llm"` 时的模型服务配置;`advanced` 打开输入框上方的「高级设置」。 */
10
+ llm?: LlmChatOptions & {
11
+ advanced?: boolean | LlmAdvancedSettings;
12
+ onOverrideChange?: (override: LlmOverride) => void;
13
+ };
14
+ /** `mode="llm"` 时拿到操作句柄;智能体模式请用 `onSessionReady`。 */
15
+ onLlmReady?: (handle: LlmChatHandle) => void;
38
16
  }
39
17
  /**
40
- * 开箱即用的聊天界面:连接(或新建)一个会话,渲染消息流与输入框。
41
- * 状态全部来自 useAgentSession,不依赖任何全局 store。
18
+ * 聊天界面。默认接 Blade 智能体会话,`mode="llm"` 时改接标准 OpenAI 对话接口。
19
+ *
20
+ * 这一层只做分流:两种模式各自是独立组件,已经确定要哪一种的可以直接用
21
+ * `<AgentChat>` 或 `<LlmChat>`。
42
22
  */
43
23
  export declare function ChatView(props: ChatViewProps): import("react/jsx-runtime").JSX.Element;
@@ -3,6 +3,6 @@ interface Props {
3
3
  connection: ConnectionStatus;
4
4
  className?: string;
5
5
  }
6
- /** 连接状态提示条:短暂断线静默,超过容错时间后再逐级提示。 */
6
+ /** 连接状态提示条:数据来自 SessionState.connection,正常连接时不渲染。 */
7
7
  export declare function ConnectionBanner({ connection, className }: Props): import("react/jsx-runtime").JSX.Element | null;
8
8
  export {};
@@ -8,9 +8,9 @@ export type FileCardProps = ComponentPropsWithRef<"span"> & ExtraProps & {
8
8
  sessionId?: string;
9
9
  };
10
10
  /**
11
- * 智能体产出文件的下载链接(由 MEDIA 行折叠而来)。
11
+ * 智能体产出文件的下载卡片(由 MEDIA 行折叠而来)。
12
12
  *
13
- * href 让 Vue/纯 HTML 宿主看到标准的「文件名→URL」语义;普通点击仍交给
14
- * downloadFile,避免跨域时 download 属性被忽略,也确保使用点击时的最新 token。
13
+ * 整张卡片只有一个动作:下载。第一方界面点卡片是「在右侧预览」,那依赖 app 的 artifact
14
+ * 面板,SDK 里没有这块,所以不摆预览入口也不写预览文案——UI 不能承诺不存在的能力。
15
15
  */
16
16
  export declare function FileCard({ node, "data-path": pathAttribute, "data-name": nameAttribute, dataPath, dataName, sessionId, children, className, ...props }: FileCardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,40 @@
1
+ /** 纯 LLM 模式的高级设置:让用户自己改模型服务地址。 */
2
+ export interface LlmAdvancedSettings {
3
+ /** 允许改模型服务地址,默认允许。 */
4
+ baseURL?: boolean;
5
+ /** 允许改模型名,默认允许。 */
6
+ model?: boolean;
7
+ /**
8
+ * 允许在界面上填密钥,默认**不允许**。
9
+ * 打开意味着密钥存在浏览器里,只适合受控内网、演示,或密钥本身就属于当前用户的场景。
10
+ */
11
+ apiKey?: boolean;
12
+ /** 覆盖值存哪:`local` 记住用户的选择(默认),`memory` 只在本次会话有效。 */
13
+ storage?: "local" | "memory";
14
+ /** localStorage 的键,默认按组件拿到的 baseURL 生成。 */
15
+ storageKey?: string;
16
+ }
17
+ export interface LlmOverride {
18
+ baseURL?: string;
19
+ model?: string;
20
+ apiKey?: string;
21
+ }
22
+ export declare function normalizeAdvanced(value: boolean | LlmAdvancedSettings | undefined): Required<Pick<LlmAdvancedSettings, "baseURL" | "model" | "apiKey" | "storage">> | null;
23
+ export declare function readOverride(settings: boolean | LlmAdvancedSettings | undefined, baseURL: string): LlmOverride;
24
+ declare function writeOverride(settings: boolean | LlmAdvancedSettings | undefined, baseURL: string, override: LlmOverride): void;
25
+ interface Props {
26
+ settings: boolean | LlmAdvancedSettings;
27
+ /** 组件拿到的默认值,用来做占位提示与"恢复默认" */
28
+ defaults: {
29
+ baseURL: string;
30
+ model: string;
31
+ };
32
+ override: LlmOverride;
33
+ onChange: (next: LlmOverride) => void;
34
+ }
35
+ /**
36
+ * 输入框上方的一行「高级设置」:默认收起,展开后可以改模型服务地址与模型。
37
+ * 只有调用方显式打开才渲染——不传 advanced 时这里一个节点都不会出现。
38
+ */
39
+ export declare function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }: Props): import("react/jsx-runtime").JSX.Element | null;
40
+ export { writeOverride };
@@ -0,0 +1,29 @@
1
+ import { type LlmChatOptions } from "../hooks/use-llm-chat";
2
+ import { type ChatPresentationProps } from "./ChatSurface";
3
+ import { type LlmAdvancedSettings, type LlmOverride } from "./LlmAdvancedSettings";
4
+ /** 纯 LLM 模式的操作句柄,对应智能体模式的 session.insertText() / send()。 */
5
+ export interface LlmChatHandle {
6
+ /** 把文本放进输入框交给用户确认,不直接发出去。 */
7
+ insertText: (text: string) => void;
8
+ send: (text: string) => Promise<boolean>;
9
+ /** 清空这段对话。 */
10
+ reset: () => void;
11
+ }
12
+ export interface LlmChatProps extends LlmChatOptions, ChatPresentationProps {
13
+ /** 拿到操作句柄:业务界面把选中的数据塞进输入框,或直接发一句。 */
14
+ onReady?: (handle: LlmChatHandle) => void;
15
+ /**
16
+ * 打开输入框上方的「高级设置」,让用户自己改模型服务地址(默认还能改模型名)。
17
+ * 不传就一个节点都不渲染。传 `true` 用默认配置,或用对象细调。
18
+ */
19
+ advanced?: boolean | LlmAdvancedSettings;
20
+ /** 用户改了覆盖值时回调,方便宿主自己也记一份。 */
21
+ onOverrideChange?: (override: LlmOverride) => void;
22
+ }
23
+ /**
24
+ * 纯 LLM 模式的聊天界面:不建会话、不连实时通道、也不要求登录 Blade,
25
+ * 只按 **OpenAI Chat Completions** 标准协议跟模型服务一问一答。
26
+ *
27
+ * 默认形态是把 `baseURL` 指向自己后端的透传路径,由后端补密钥再转发,浏览器不持有密钥。
28
+ */
29
+ export declare function LlmChat({ classNames, renderers, slots, placeholder, theme, onReady, advanced, onOverrideChange, ...options }: LlmChatProps): import("react/jsx-runtime").JSX.Element;
@@ -4,13 +4,13 @@ export interface MarkdownContentProps {
4
4
  /** 流式渲染传 "streaming",历史静态内容默认 "static"。 */
5
5
  mode?: "streaming" | "static";
6
6
  /**
7
- * 会话 id。传了才会把 `MEDIA:` 行折叠成文件下载链接——下载要用它。
7
+ * 会话 id。传了才会把 `MEDIA:` 行折叠成文件下载卡片——下载要用它。
8
8
  * 不要给用户消息传:用户自己在输入框打一行 `MEDIA:` 不该被当成平台产物。
9
9
  */
10
10
  sessionId?: string;
11
11
  }
12
12
  /**
13
- * SDK 版 Markdown 渲染:附带代码块复制按钮、智能体产出文件的下载链接;
13
+ * SDK 版 Markdown 渲染:附带代码块复制按钮、智能体产出文件的下载卡片;
14
14
  * 相比第一方版本去掉了 mermaid、外链确认弹窗与文件预览。
15
15
  */
16
16
  export declare function MarkdownContent({ children, className, mode, sessionId }: MarkdownContentProps): import("react/jsx-runtime").JSX.Element;
@@ -10,7 +10,7 @@ interface Props {
10
10
  toolCallRenderer?: ToolCallRenderer;
11
11
  emptyState?: ReactNode;
12
12
  className?: string;
13
- /** 传给助手消息的 Markdown 渲染,用于把 MEDIA 行折叠成文件下载链接。 */
13
+ /** 传给助手消息的 Markdown 渲染,用于把 MEDIA 行折叠成文件下载卡片。 */
14
14
  sessionId?: string;
15
15
  }
16
16
  /**
@@ -0,0 +1,13 @@
1
+ import type { ReplaySpeed } from "@blade-hq/agent-client";
2
+ export interface ReplayBarProps {
3
+ /** 非回放会话传 false 时整条不渲染。 */
4
+ isReplay: boolean;
5
+ speed: ReplaySpeed;
6
+ onSpeedChange: (speed: ReplaySpeed) => void;
7
+ onExit: () => void;
8
+ /** 传 useReplay().canControl。只读身份下仍然显示"在回放",但操作按钮禁用。 */
9
+ canControl?: boolean;
10
+ className?: string;
11
+ }
12
+ /** 回放会话的顶部状态条:说明当前在回放、切换倍速、随时退出回放。 */
13
+ export declare function ReplayBar({ isReplay, speed, onSpeedChange, onExit, canControl, className, }: ReplayBarProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,8 @@
1
+ import type { ReplayMismatch } from "../hooks/use-replay";
2
+ export interface ReplayMismatchPromptProps {
3
+ /** 来自 useReplay().mismatch;为 null 时不渲染。 */
4
+ mismatch: ReplayMismatch | null;
5
+ className?: string;
6
+ }
7
+ /** 回放中输入和录制内容对不上时的选择卡:按录制继续,还是从这里真跑。 */
8
+ export declare function ReplayMismatchPrompt({ mismatch, className }: ReplayMismatchPromptProps): import("react/jsx-runtime").JSX.Element | null;
@@ -18,15 +18,13 @@ declare function getChat(element: Element | null): Promise<AgentSession>;
18
18
  declare const BladeAgent: {
19
19
  BladeClient: typeof agentClient.BladeClient;
20
20
  BladeApiError: typeof agentClient.BladeApiError;
21
- EMPTY_PLATFORM_ENDPOINTS: agentClient.PlatformEndpoints;
22
- loadPlatformEndpoints: typeof agentClient.loadPlatformEndpoints;
23
- resolveServiceUrl: typeof agentClient.resolveServiceUrl;
24
21
  SDK_NAME: "agent-client";
25
22
  SDK_VERSION: string;
26
23
  AgentSession: typeof AgentSession;
27
24
  SessionHub: typeof agentClient.SessionHub;
28
25
  SessionSetupError: typeof agentClient.SessionSetupError;
29
26
  createInitialSessionState: typeof agentClient.createInitialSessionState;
27
+ toReplaySnapshot: typeof agentClient.toReplaySnapshot;
30
28
  connectEmbedded: typeof agentClient.connectEmbedded;
31
29
  isCommandEnvelope: typeof agentClient.isCommandEnvelope;
32
30
  isInboundEnvelope: typeof agentClient.isInboundEnvelope;
@@ -41,6 +39,7 @@ declare const BladeAgent: {
41
39
  isHiddenInternalMessage: typeof agentClient.isHiddenInternalMessage;
42
40
  normalizeMessageContent: typeof agentClient.normalizeMessageContent;
43
41
  transformSlashCommand: typeof agentClient.transformSlashCommand;
42
+ DEFAULT_REPLAY_SPEED: agentClient.ReplaySpeed;
44
43
  SessionInfo: import("arktype/internal/variants/object.ts").ObjectType<{
45
44
  id: string;
46
45
  intent: string;
@@ -0,0 +1,57 @@
1
+ import type { ChatMessage } from "@blade-hq/agent-client";
2
+ /** OpenAI 标准的工具声明,原样发给模型服务。 */
3
+ export interface ChatCompletionTool {
4
+ type: "function";
5
+ function: {
6
+ name: string;
7
+ description?: string;
8
+ parameters?: Record<string, unknown>;
9
+ };
10
+ }
11
+ export interface LlmToolCall {
12
+ id: string;
13
+ name: string;
14
+ /** 模型生成的 JSON 字符串,是不可信输入,用之前自己校验。 */
15
+ arguments: string;
16
+ }
17
+ export interface LlmChatOptions {
18
+ /** OpenAI 兼容根地址,如 `/api/llm`(自己后端的透传路径)或 `http://host:30000/v1`。 */
19
+ baseURL: string;
20
+ model: string;
21
+ /**
22
+ * 直连模型服务时才需要。默认形态是让 `baseURL` 指向自己的后端,由后端补密钥转发,
23
+ * 浏览器不持有密钥。
24
+ */
25
+ apiKey?: string;
26
+ headers?: Record<string, string>;
27
+ /** 系统提示词,合成到消息最前面。 */
28
+ system?: string;
29
+ /** 每次带上最近多少轮,默认 12。越长越贵,也越容易超上下文。 */
30
+ historyTurns?: number;
31
+ temperature?: number;
32
+ /** 透传给模型服务的额外字段。 */
33
+ extraBody?: Record<string, unknown>;
34
+ tools?: ChatCompletionTool[];
35
+ /** 执行模型请求的工具,返回值会被序列化后回喂给模型。 */
36
+ onToolCall?: (call: LlmToolCall) => Promise<unknown>;
37
+ /** 工具最多来回几轮,默认 4,防止模型绕圈烧 token。 */
38
+ maxToolRounds?: number;
39
+ fetchImpl?: typeof fetch;
40
+ }
41
+ export interface UseLlmChatResult {
42
+ messages: ChatMessage[];
43
+ isStreaming: boolean;
44
+ /** 出错原因;下一次发送会清掉。 */
45
+ error: string | null;
46
+ send: (text: string) => Promise<boolean>;
47
+ stop: () => void;
48
+ reset: () => void;
49
+ }
50
+ /**
51
+ * 纯 LLM 对话的状态机,协议就是 **OpenAI Chat Completions**——任何兼容实现都能直接接,
52
+ * 不需要为这个组件专门写后端。
53
+ *
54
+ * 工具调用走 OpenAI 标准的多轮流程:模型返回 tool_calls → 调 onToolCall 执行 →
55
+ * 结果作为 role:"tool" 消息回喂 → 再请求一次。
56
+ */
57
+ export declare function useLlmChat(options: LlmChatOptions): UseLlmChatResult;
@@ -0,0 +1,50 @@
1
+ import type { AgentSession, ReplaySpeed } from "@blade-hq/agent-client";
2
+ /** 回放中用户说的话和录制内容对不上,需要当场决定往哪走。 */
3
+ export interface ReplayMismatch {
4
+ /** 用户这次实际发出的内容。 */
5
+ actualMessage: string;
6
+ /** 源会话里录制的下一句。 */
7
+ expectedMessage: string;
8
+ /** keep_replay:按录制内容继续;continue_replay:从这里转为真实运行。 */
9
+ resolve: (decision: "keep_replay" | "continue_replay") => void;
10
+ }
11
+ export interface UseReplayResult {
12
+ /** 当前会话是不是正在回放(转自主运行后为 false)。 */
13
+ isReplay: boolean;
14
+ /** 当前播放倍速,非回放会话固定为 1。 */
15
+ speed: ReplaySpeed;
16
+ setSpeed: (speed: ReplaySpeed) => Promise<void>;
17
+ /** 退出回放,之后的对话交给真实模型运行。 */
18
+ exitToAutonomous: () => Promise<void>;
19
+ /** 待决策的输入冲突;处理完自动清空。 */
20
+ mismatch: ReplayMismatch | null;
21
+ /** 能不能改回放(改回放是 owner 专属)。只读身份下仍然显示"在回放",但不能操作。 */
22
+ canControl: boolean;
23
+ /** 当前会话能否作为回放来源(并行子智能体等场景不支持)。 */
24
+ canReplay: boolean;
25
+ /** canReplay 为 false 时的原因,可直接展示。 */
26
+ unsupportedReason: string | null;
27
+ /** 从当前会话派生一个回放会话,返回新会话 id。 */
28
+ startReplay: (speed?: ReplaySpeed) => Promise<string>;
29
+ isStarting: boolean;
30
+ /** 最近一次回放操作失败的原因。 */
31
+ error: Error | null;
32
+ }
33
+ /**
34
+ * 会话回放(演示 / 彩排模式)。
35
+ *
36
+ * 回放会话不会自动播放:用户照常发消息,只要这句和源会话录制的下一句意图一致,
37
+ * 服务端就按倍速重放当时的回复和工具调用,完全不调用模型;对不上就抛出
38
+ * `mismatch` 交给界面决定。
39
+ *
40
+ * 同一个 hook 覆盖两种身份——传入的会话既可能是回放会话(isReplay / setSpeed /
41
+ * exitToAutonomous / mismatch),也可能是准备被回放的源会话(canReplay /
42
+ * startReplay)。
43
+ *
44
+ * 回放状态本身住在 `SessionState.replay` 里(非 React 宿主直接订阅它即可),
45
+ * 这里只做 React 绑定:订阅快照 + 接管冲突 + 拉一次 preview。
46
+ *
47
+ * **注册即接管**:底层的 `replayMismatch` 只交给最先注册的处理器。调用本 hook
48
+ * 就意味着由它接管冲突;不调用则维持 SDK 默认行为——直接转为真实运行。
49
+ */
50
+ export declare function useReplay(session: AgentSession | null): UseReplayResult;
package/dist/index.d.ts CHANGED
@@ -2,9 +2,22 @@ export { BladeProvider, useBladeClient } from "./context";
2
2
  export type { BladeProviderProps } from "./context";
3
3
  export { useAgentSession } from "./hooks/use-agent-session";
4
4
  export type { UseAgentSessionOptions, UseAgentSessionResult } from "./hooks/use-agent-session";
5
+ export { useReplay } from "./hooks/use-replay";
6
+ export type { ReplayMismatch, UseReplayResult } from "./hooks/use-replay";
7
+ export { useLlmChat } from "./hooks/use-llm-chat";
8
+ export type { ChatCompletionTool, LlmChatOptions, LlmToolCall, UseLlmChatResult, } from "./hooks/use-llm-chat";
5
9
  export { ChatView } from "./components/ChatView";
6
10
  export type { ChatViewClassNames, ChatViewProps, ChatViewRenderers, ChatViewSlots, } from "./components/ChatView";
11
+ export { AgentChat } from "./components/AgentChat";
12
+ export type { AgentChatProps } from "./components/AgentChat";
13
+ export { LlmChat } from "./components/LlmChat";
14
+ export type { LlmChatHandle, LlmChatProps } from "./components/LlmChat";
15
+ export type { LlmAdvancedSettings, LlmOverride } from "./components/LlmAdvancedSettings";
7
16
  export { MarkdownContent } from "./components/MarkdownContent";
8
17
  export type { MarkdownContentProps } from "./components/MarkdownContent";
18
+ export { ReplayBar } from "./components/ReplayBar";
19
+ export type { ReplayBarProps } from "./components/ReplayBar";
20
+ export { ReplayMismatchPrompt } from "./components/ReplayMismatchPrompt";
21
+ export type { ReplayMismatchPromptProps } from "./components/ReplayMismatchPrompt";
9
22
  export type { ToolCallRenderer } from "./components/ToolCallBlock";
10
23
  export * from "@blade-hq/agent-client";