@blade-hq/agent-react 2608.0.5 → 2608.0.7-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
@@ -28,6 +28,7 @@ export function App() {
28
28
 
29
29
  - 不传 `sessionId` 自动创建新会话;未登录时 `ChatView` 自己渲染登录按钮(弹窗授权,见 agent-client 的 `client.auth.login()`)。
30
30
  - 同一 `BladeProvider` 下可以放多个 `ChatView`,各自独立会话、互不干扰。
31
+ - 宿主只需要完成态消息时,可在 `BladeClient` 构造参数中传 `streamTokens: false`,不订阅逐 token 增量。
31
32
 
32
33
  ## BladeProvider
33
34
 
@@ -68,6 +69,66 @@ await session?.send("你好")
68
69
  - 自动创建只发生一次(含 React StrictMode 双跑);创建出的 id 通过 `onSessionCreated` 交还,重挂载时把它作为 `sessionId` 传回即可复用会话。
69
70
  - 同一 `sessionId` 的多次调用复用同一 `AgentSession` 实例(引用计数);卸载后延迟释放,路由抖动不会断连。
70
71
 
72
+ ## useReplay
73
+
74
+ 会话回放(演示 / 彩排):拿一个已经聊完的会话当素材,重现当时的回复和工具调用,
75
+ **完全不调用模型**。
76
+
77
+ **进入回放是"派生一个新会话",不是把当前会话切模式**:`startReplay()` 返回的是新会话
78
+ 的 id,拿它去渲染(`<ChatView sessionId={newId} />` 或自己 `connect`)才进入回放。源会话
79
+ 原样不动。连上之后也不会自动播——用户照着源会话的问题问,才逐句重现。
80
+
81
+ 同一个 hook 覆盖两种身份——传进去的会话既可能是准备被回放的**源会话**,也可能是**回放中**的会话:
82
+
83
+ ```tsx
84
+ // —— 源会话页面:提供"开始回放"入口 ——
85
+ const { session: sourceSession } = useAgentSession(sourceId)
86
+ const source = useReplay(sourceSession)
87
+
88
+ source.canReplay // false 时 source.unsupportedReason 可直接展示
89
+ const replayId = await source.startReplay(5) // 返回**新建的**回放会话 id,路由过去
90
+
91
+ // —— 回放会话页面:状态与控制 ——
92
+ const { session: replaySession } = useAgentSession(replayId)
93
+ const replay = useReplay(replaySession)
94
+
95
+ replay.isReplay // 是不是在回放
96
+ replay.speed // 当前倍速
97
+ replay.canControl // 改回放是 owner 专属;分享出去的只读会话为 false
98
+ replay.mismatch // 输入和录制对不上时的待决策项
99
+ await replay.setSpeed(2)
100
+ await replay.exitToAutonomous() // 退出回放,之后真的运行
101
+ ```
102
+
103
+ 配套两个组件,直接接上 `useReplay()` 的返回值即可:
104
+
105
+ ```tsx
106
+ <ReplayBar
107
+ isReplay={replay.isReplay}
108
+ speed={replay.speed}
109
+ canControl={replay.canControl}
110
+ onSpeedChange={(s) => void replay.setSpeed(s)}
111
+ onExit={() => void replay.exitToAutonomous()}
112
+ />
113
+ <ReplayMismatchPrompt mismatch={replay.mismatch} />
114
+ ```
115
+
116
+ 返回值见 `UseReplayResult`,冲突项见 `ReplayMismatch`,组件属性见 `ReplayBarProps` /
117
+ `ReplayMismatchPromptProps`。
118
+
119
+ 只读身份(分享链接打开的会话)仍然会显示「回放模式」——这是会话事实,藏起来会让分享出去的
120
+ 回放看着像真实运行;但倍速和退出会被禁用,因为服务端的 `PATCH` 是 owner 专属。
121
+
122
+ **`ChatView` 已经自带这一整套**——用它打开回放会话,状态条、倍速、退出、冲突卡片都在,
123
+ 不用自己接。下面这些 API 是给自建 UI 用的。
124
+
125
+ **注册即接管**:底层的 `replayMismatch` 只交给最先注册的处理器。调用 `useReplay()`(或用
126
+ `ChatView`)就意味着由它接管冲突;两者都不用则维持 SDK 默认行为——静默转为真实运行。
127
+
128
+ 回放状态本身住在 `SessionState.replay` 里,本 hook 只是它的 React 绑定。非 React 宿主
129
+ (Vue、Svelte、自建 UI)直接订阅快照即可,不需要这个包,用法见
130
+ [agent-client 的会话回放章节](../agent-client/README.md#会话回放演示--彩排)。
131
+
71
132
  ## ChatView
72
133
 
73
134
  完整聊天界面:消息列表(Markdown、代码高亮、工具调用、提问卡片)+ 输入框 + 连接状态条 + 登录引导。
@@ -79,6 +140,7 @@ await session?.send("你好")
79
140
  onSessionCreated={(id) => ...} // 自动创建的会话 id 交还调用方
80
141
  onSessionReady={(session) => ...} // 会话实例就绪(页面协作高级用法)
81
142
  commands={{ "map.highlight": (data) => map.highlight(data) }} // 智能体 → 页面指令
143
+ onFollowupInteraction={(event) => analytics.capture(event.type, event)} // 厂商无关交互回调
82
144
  placeholder="输入消息..."
83
145
  classNames={{ root: "...", banner: "...", messages: "...", input: "..." }} // ChatViewClassNames
84
146
  renderers={{ toolCall: (info) => <MyToolCard info={info} /> }} // ChatViewRenderers
@@ -88,6 +150,9 @@ await session?.send("你好")
88
150
 
89
151
  全部 props 见 `ChatViewProps`。样式说明:
90
152
 
153
+ `onFollowupInteraction` 使用 `FollowupInteractionEvent`,覆盖下一步建议展示/采纳、成果展示/打开/下载以及结果评分。SDK 不内置 PostHog 等分析厂商;宿主回调抛错也不会中断用户点击或下载。
154
+ 文件成果以文件名为文本的标准下载链接展示;Vue / 纯 HTML 使用的 `<blade-chat>` 与 `ChatView` 行为一致。
155
+
91
156
  - 必须引入一份样式,按宿主有没有 Tailwind 二选一:
92
157
  - **`style.full.css`**(默认选它):布局兜底 + 编译好的 Tailwind 产物,自包含,宿主没装 Tailwind 也是完整视觉。
93
158
  - **`style.css`**:只有 `.blade-chat-*` 布局兜底。宿主自己装了 Tailwind、且 `content` 配了扫描 `node_modules/@blade-hq/agent-react` 时用它,可以少一份重复 CSS。选错不会报错,只是工具块、思考块内部会退化成没有布局的裸 HTML。
@@ -96,6 +161,95 @@ await session?.send("你好")
96
161
  - `renderers.toolCall` 返回 `null` 时回落到默认工具卡片。
97
162
  - **内边距按容器宽度伸缩**,嵌进窄侧栏(300–400px)时会自动收窄,不看浏览器视口。代价是聊天容器需要有确定宽度——放在 flex/grid 里被拉伸,或显式给宽度都可以;放进 `width: fit-content`、`inline-flex` 这类由内容决定宽度的父容器会塌缩。
98
163
 
164
+ ## 纯 LLM 模式(不建会话)
165
+
166
+ 有些应用只要「一问一答」——分类、改写、总结,不需要智能体、不需要会话、也不需要用户登录
167
+ Blade。这时把 `ChatView` 切到 `mode="llm"`,或者直接用下层的 `LlmChat`,界面完全一样:
168
+
169
+ ```tsx
170
+ <ChatView /> // 默认:智能体模式,行为不变
171
+ <ChatView mode="llm" llm={{ baseURL: "/api/llm", model }} /> // 切成纯 LLM
172
+ <LlmChat baseURL="/api/llm" model={model} /> // 或者直接用(LlmChatProps)
173
+ <AgentChat sessionId={id} /> // 智能体那侧同样可以直接用(AgentChatProps)
174
+ ```
175
+
176
+ **协议就是 OpenAI 的**:`POST {baseURL}/chat/completions`,`stream: true`,读
177
+ `choices[0].delta`。任何 OpenAI 兼容实现(Blade 网关、vLLM、Ollama、OpenAI 本身、你自己的
178
+ 透传代理)都能直接接,不用为这个组件专门写后端。
179
+
180
+ **密钥默认不进浏览器**:把 `baseURL` 指向自己后端的一个路径,后端补上 `Authorization` 再原样
181
+ 转发即可(协议是标准的,这个代理就是纯转发)。确实要前端直连时才传 `apiKey`——只适合受控内网、
182
+ 演示,或密钥本身就属于当前用户的场景。
183
+
184
+ **模型服务地址和模型名别写死**,用 `client.models.list()` 拿,各部署端口不一样:
185
+
186
+ ```tsx
187
+ const { baseUrl, defaultServiceModel, models } = await client.models.list()
188
+
189
+ // 后端透传(推荐):baseURL 指向自己的路径,模型名仍然要用 defaultServiceModel
190
+ <ChatView mode="llm" llm={{ baseURL: "/api/llm", model: defaultServiceModel }} />
191
+
192
+ // 浏览器直连:仅当 baseUrl 对浏览器也可达时
193
+ <ChatView mode="llm" llm={{ baseURL: baseUrl, model: defaultServiceModel, apiKey }} />
194
+
195
+ // 让用户选模型时,过滤掉不在这个服务上的
196
+ models.filter((m) => m.serviceModelId)
197
+ ```
198
+
199
+ **模型名要用 `defaultServiceModel` / `serviceModelId`,不是 `default` / `id`。** 后者是平台内部
200
+ 标识,平台接了多个模型服务时形如 `provider-xxx::deepseek-v4-flash`,发给模型服务只会得到
201
+ 「模型不存在」。`baseUrl` 同理是服务端视角的地址,平台常配成 `http://127.0.0.1:30000/v1`,
202
+ 那是给自己后端转发用的,不能直接丢给浏览器。
203
+
204
+ **工具调用走 OpenAI 标准的多轮流程**,不需要任何私有扩展:声明 `tools`,在 `onToolCall`
205
+ (收到 `LlmToolCall`)里执行——通常就是调你自己应用的同源接口——返回值会作为
206
+ `role: "tool"` 消息回喂给模型,直到它给出最终回答或达到 `maxToolRounds`(默认 4)。
207
+ 每一轮工具调用都会渲染成和智能体模式一样的工具卡片。
208
+
209
+ ```tsx
210
+ <LlmChat
211
+ baseURL="/api/llm"
212
+ model={model}
213
+ system="你是订单助手"
214
+ tools={[{ type: "function", function: { name: "query_orders", parameters: schema } }]} // ChatCompletionTool
215
+ onToolCall={async ({ name, arguments: args }) => {
216
+ if (name !== "query_orders") throw new Error(`没有这个函数:${name}`)
217
+ // 参数是模型生成的不可信输入:先校验类型,再用 URLSearchParams 拼,
218
+ // 直接塞进查询串的话,一个 `李航&limit=all` 就能多带出参数
219
+ const { owner } = JSON.parse(args || "{}")
220
+ if (typeof owner !== "string") throw new Error("owner 必须是字符串")
221
+ const query = new URLSearchParams({ owner })
222
+ const resp = await fetch(`/api/orders?${query}`)
223
+ if (!resp.ok) throw new Error(`订单接口返回 ${resp.status}`)
224
+ return resp.json()
225
+ }}
226
+ onReady={(handle) => setChat(handle)} // LlmChatHandle:insertText / send / reset
227
+ />
228
+ ```
229
+
230
+ ### 让用户自己改模型服务地址
231
+
232
+ 传 `advanced` 就在输入框上方多一行「高级设置」(默认收起),用户可以改模型服务地址和模型名;
233
+ 改过之后会显示「已自定义」,也能一键恢复默认。不传就一个节点都不渲染。
234
+
235
+ ```tsx
236
+ <LlmChat baseURL="/api/llm" model={model} advanced />
237
+ <LlmChat baseURL="/api/llm" model={model} advanced={{ apiKey: true, storage: "memory" }} />
238
+ <ChatView mode="llm" llm={{ baseURL: "/api/llm", model, advanced: true }} />
239
+ ```
240
+
241
+ `LlmAdvancedSettings` 可细调:`baseURL` / `model` 各自能不能改(默认都能)、`apiKey` 要不要
242
+ 出现(**默认不出现**,打开意味着密钥存在浏览器里,只适合受控场景)、`storage` 是记住选择
243
+ (`local`,默认)还是只在本次有效(`memory`)、`storageKey` 自定义存储键。用户改了会回调
244
+ `onOverrideChange`(`LlmOverride`),宿主想自己也记一份时用。
245
+
246
+ 其余可选项见 `LlmChatOptions`:`headers`、`historyTurns`(默认 12 轮)、`temperature`、
247
+ `extraBody`、`fetchImpl`。只要状态机、界面自己写的,用 `useLlmChat`(返回
248
+ `UseLlmChatResult`:`messages` / `isStreaming` / `error` / `send` / `stop` / `reset`)。
249
+
250
+ 纯 LLM 模式不做会话持久化、文件上传、提问卡片、记忆和检查点——那些是智能体协议的能力,
251
+ 需要就用默认模式。
252
+
99
253
  ## MarkdownContent
100
254
 
101
255
  单独渲染一段智能体消息 Markdown(含代码复制按钮、XSS 消毒;HTML 经 sanitize 白名单处理,不接受任意 raw HTML):
@@ -104,8 +258,6 @@ await session?.send("你好")
104
258
  <MarkdownContent content={markdownText} />
105
259
  ```
106
260
 
107
- 传了 `sessionId` 才会把智能体产出文件的 `MEDIA:` 行折叠成下载卡片(下载要用它)。`ChatView` 内部已经传好,只有单独用 `MarkdownContent` 时需要自己给:
108
-
109
261
  ```tsx
110
262
  <MarkdownContent sessionId={sessionId}>{markdownText}</MarkdownContent>
111
263
  ```
@@ -153,4 +305,4 @@ blade-chat { --primary: 262 83% 58%; height: 640px; }
153
305
 
154
306
  为方便单包引入,本包 re-export 了 agent-client 的全部公开声明(`BladeClient`、`AgentSession`、`SessionState`、协议类型等),文档一律见 [agent-client 的 README](../agent-client/README.md)。完整签名见 [public-api.md](./public-api.md)。
155
307
 
156
- 本包自有声明:`BladeProvider`、`BladeProviderProps`、`useBladeClient`、`useAgentSession`、`UseAgentSessionOptions`、`UseAgentSessionResult`、`ChatView`、`ChatViewProps`、`ChatViewClassNames`、`ChatViewRenderers`、`ChatViewSlots`、`ToolCallRenderer`、`MarkdownContent`、`MarkdownContentProps`。
308
+ 本包自有声明:`BladeProvider`、`BladeProviderProps`、`useBladeClient`、`useAgentSession`、`UseAgentSessionOptions`、`UseAgentSessionResult`、`useReplay`、`UseReplayResult`、`ReplayMismatch`、`ReplayBar`、`ReplayBarProps`、`ReplayMismatchPrompt`、`ReplayMismatchPromptProps`、`ChatView`、`ChatViewProps`、`ChatViewClassNames`、`ChatViewRenderers`、`ChatViewSlots`、`FollowupInteractionEvent`、`ToolCallRenderer`、`MarkdownContent`、`MarkdownContentProps`。
@@ -0,0 +1,21 @@
1
+ import type { AgentSession, CreateSessionRequest } from "@blade-hq/agent-client";
2
+ import { type ChatPresentationProps } from "./ChatSurface";
3
+ import type { FollowupInteractionEvent } from "./PostChatFollowupBlock";
4
+ export interface AgentChatProps extends ChatPresentationProps {
5
+ /** follow-up、成果和评分交互;SDK 不绑定任何分析厂商。 */
6
+ onFollowupInteraction?: (event: FollowupInteractionEvent) => void;
7
+ /** 连接既有会话;不传则按 createOptions 自动新建。 */
8
+ sessionId?: string;
9
+ createOptions?: CreateSessionRequest;
10
+ /** 自动创建出的 sessionId 通过此回调交还调用方持有。 */
11
+ onSessionCreated?: (sessionId: string) => void;
12
+ /** 会话连接完成后回调(页面协作场景拿 AgentSession 实例用)。 */
13
+ onSessionReady?: (session: AgentSession) => void;
14
+ /** 智能体下发给宿主页面的指令处理器(action → handler)。 */
15
+ commands?: Record<string, (data: unknown) => void>;
16
+ }
17
+ /**
18
+ * 智能体模式的聊天界面:连接(或新建)一个 Blade 会话,渲染消息流与输入框。
19
+ * 状态全部来自 useAgentSession,不依赖任何全局 store。
20
+ */
21
+ 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
+ /** 会话 id;传给 Markdown 渲染的统一上下文。 */
11
11
  sessionId?: string;
12
12
  }
13
13
  /**
@@ -0,0 +1,68 @@
1
+ import type { AskUserAnswerData, ChatMessage, ConnectionStatus, PostChatFollowup } from "@blade-hq/agent-client";
2
+ import type { ReactNode } from "react";
3
+ import type { ToolCallRenderer } from "./ToolCallBlock";
4
+ import type { FollowupInteractionEvent } from "./PostChatFollowupBlock";
5
+ import type { ResultFeedback } from "@blade-hq/agent-client";
6
+ export interface ChatViewClassNames {
7
+ root?: string;
8
+ banner?: string;
9
+ messageList?: string;
10
+ chatInput?: string;
11
+ }
12
+ export interface ChatViewRenderers {
13
+ /** 自定义工具调用渲染;返回 null 走默认渲染。 */
14
+ toolCall?: ToolCallRenderer;
15
+ }
16
+ export interface ChatViewSlots {
17
+ /** 消息列表上方的自定义区域。 */
18
+ header?: ReactNode;
19
+ /** 无消息时的空状态。 */
20
+ emptyState?: ReactNode;
21
+ /** 输入框下方的自定义区域。 */
22
+ footer?: ReactNode;
23
+ }
24
+ /** 两种模式共用的外观属性。 */
25
+ export interface ChatPresentationProps {
26
+ classNames?: ChatViewClassNames;
27
+ renderers?: ChatViewRenderers;
28
+ slots?: ChatViewSlots;
29
+ placeholder?: string;
30
+ /** 配色主题,默认浅色。宿主已经在祖先元素上写了 data-theme 时不用传。 */
31
+ theme?: "light" | "dark";
32
+ }
33
+ /** 主题落到聊天根节点上:深色变量挂在 [data-theme="dark"] 上,由它继承给整棵子树。 */
34
+ export declare function themeAttr(theme: ChatPresentationProps["theme"]): "dark" | undefined;
35
+ interface ChatSurfaceProps extends ChatPresentationProps {
36
+ /** follow-up、成果和评分交互;SDK 不绑定任何分析厂商。 */
37
+ onFollowupInteraction?: (event: FollowupInteractionEvent) => void;
38
+ connection: ConnectionStatus;
39
+ errorMessage: string | null;
40
+ messages: ChatMessage[];
41
+ postChatFollowup?: PostChatFollowup | null;
42
+ isStreaming: boolean;
43
+ isStopping: boolean;
44
+ inputText: string;
45
+ onInputChange: (value: string) => void;
46
+ onSuggestion?: (value: string) => void;
47
+ onSend: (text: string) => Promise<boolean>;
48
+ onStop: () => void;
49
+ sessionStatus?: string;
50
+ askAnswers?: Record<string, AskUserAnswerData>;
51
+ onAnswer?: (answer: string, toolCallId: string, answerData: AskUserAnswerData) => void;
52
+ sessionId?: string;
53
+ isViewer?: boolean;
54
+ resultFeedbackByEntry?: ReadonlyMap<string, ResultFeedback>;
55
+ onResultFeedbackSaved?: (feedback: ResultFeedback) => void;
56
+ /** 输入框上方的内部区域(纯 LLM 模式的高级设置用)。不传时不渲染任何节点。 */
57
+ beforeInput?: ReactNode;
58
+ /** 连接状态条下方的横幅(回放模式条用)。不传时不渲染任何节点。 */
59
+ banner?: ReactNode;
60
+ }
61
+ /**
62
+ * 消息流 + 输入框:智能体模式与纯 LLM 模式共用同一套外观,差别只在谁喂数据。
63
+ *
64
+ * 这里的 DOM 结构受 tests/chat-view-compat.test.tsx 的快照保护——接入方拿
65
+ * .blade-chat-* 类名写了自己的样式,结构一动他们的页面就歪。
66
+ */
67
+ export declare function ChatSurface({ theme, classNames, renderers, slots, placeholder, connection, errorMessage, messages, postChatFollowup, isStreaming, isStopping, inputText, onInputChange, onSuggestion, onSend, onStop, sessionStatus, askAnswers, onAnswer, sessionId, isViewer, resultFeedbackByEntry, onResultFeedbackSaved, onFollowupInteraction, beforeInput, banner, }: ChatSurfaceProps): import("react/jsx-runtime").JSX.Element;
68
+ export {};
@@ -1,43 +1,24 @@
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 type { FollowupInteractionEvent } from "./PostChatFollowupBlock";
7
+ export interface ChatViewProps extends AgentChatProps {
8
+ /** 挂哪个下层组件。默认 `agent`——不传就是接 Blade 智能体会话的原有行为。 */
9
+ mode?: "agent" | "llm";
10
+ /** `mode="llm"` 时的模型服务配置;`advanced` 打开输入框上方的「高级设置」。 */
11
+ llm?: LlmChatOptions & {
12
+ advanced?: boolean | LlmAdvancedSettings;
13
+ onOverrideChange?: (override: LlmOverride) => void;
14
+ };
15
+ /** `mode="llm"` 时拿到操作句柄;智能体模式请用 `onSessionReady`。 */
16
+ onLlmReady?: (handle: LlmChatHandle) => void;
38
17
  }
39
18
  /**
40
- * 开箱即用的聊天界面:连接(或新建)一个会话,渲染消息流与输入框。
41
- * 状态全部来自 useAgentSession,不依赖任何全局 store。
19
+ * 聊天界面。默认接 Blade 智能体会话,`mode="llm"` 时改接标准 OpenAI 对话接口。
20
+ *
21
+ * 这一层只做分流:两种模式各自是独立组件,已经确定要哪一种的可以直接用
22
+ * `<AgentChat>` 或 `<LlmChat>`。
42
23
  */
43
24
  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
- /** 连接状态提示条:数据来自 SessionState.connection,正常连接时不渲染。 */
6
+ /** 连接状态提示条:短暂断线静默,超过容错时间后再逐级提示。 */
7
7
  export declare function ConnectionBanner({ connection, className }: Props): import("react/jsx-runtime").JSX.Element | null;
8
8
  export {};
@@ -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;
@@ -3,14 +3,11 @@ export interface MarkdownContentProps {
3
3
  className?: string;
4
4
  /** 流式渲染传 "streaming",历史静态内容默认 "static"。 */
5
5
  mode?: "streaming" | "static";
6
- /**
7
- * 会话 id。传了才会把 `MEDIA:` 行折叠成文件下载卡片——下载要用它。
8
- * 不要给用户消息传:用户自己在输入框打一行 `MEDIA:` 不该被当成平台产物。
9
- */
6
+ /** 会话 id;保留给聊天组件统一透传上下文。 */
10
7
  sessionId?: string;
11
8
  }
12
9
  /**
13
- * SDK 版 Markdown 渲染:附带代码块复制按钮、智能体产出文件的下载卡片;
14
- * 相比第一方版本去掉了 mermaid、外链确认弹窗与文件预览。
10
+ * SDK 版 Markdown 渲染:附带代码块复制按钮;相比第一方版本去掉了
11
+ * mermaid、外链确认弹窗与文件预览。
15
12
  */
16
13
  export declare function MarkdownContent({ children, className, mode, sessionId }: MarkdownContentProps): import("react/jsx-runtime").JSX.Element;
@@ -1,8 +1,11 @@
1
- import type { AskUserAnswerData, ChatMessage } from "@blade-hq/agent-client";
1
+ import type { AskUserAnswerData, ChatMessage, PostChatFollowup, ResultFeedback } from "@blade-hq/agent-client";
2
2
  import { type ReactNode } from "react";
3
+ import { type FollowupInteractionEvent } from "./PostChatFollowupBlock";
3
4
  import type { ToolCallRenderer } from "./ToolCallBlock";
4
5
  interface Props {
5
6
  messages: ChatMessage[];
7
+ postChatFollowup?: PostChatFollowup | null;
8
+ onSuggestion?: (value: string) => void;
6
9
  isStreaming: boolean;
7
10
  sessionStatus?: string;
8
11
  askAnswers?: Record<string, AskUserAnswerData>;
@@ -10,12 +13,16 @@ interface Props {
10
13
  toolCallRenderer?: ToolCallRenderer;
11
14
  emptyState?: ReactNode;
12
15
  className?: string;
13
- /** 传给助手消息的 Markdown 渲染,用于把 MEDIA 行折叠成文件下载卡片。 */
16
+ /** 会话 id;传给助手消息的统一上下文。 */
14
17
  sessionId?: string;
18
+ isViewer?: boolean;
19
+ onFollowupInteraction?: (event: FollowupInteractionEvent) => void;
20
+ resultFeedbackByEntry?: ReadonlyMap<string, ResultFeedback>;
21
+ onResultFeedbackSaved?: (feedback: ResultFeedback) => void;
15
22
  }
16
23
  /**
17
24
  * 消息列表:use-stick-to-bottom 自动滚动 + 按轮次分组渲染。
18
25
  * 相比第一方版本去掉了轮次导航栏、吸顶状态条与规划摘要卡片。
19
26
  */
20
- export declare function MessageList({ messages, isStreaming, sessionStatus, askAnswers, onAnswer, toolCallRenderer, emptyState, className, sessionId, }: Props): import("react/jsx-runtime").JSX.Element;
27
+ export declare function MessageList({ messages, postChatFollowup, onSuggestion, isStreaming, sessionStatus, askAnswers, onAnswer, toolCallRenderer, emptyState, className, sessionId, isViewer, onFollowupInteraction, resultFeedbackByEntry, onResultFeedbackSaved, }: Props): import("react/jsx-runtime").JSX.Element;
21
28
  export {};
@@ -0,0 +1,59 @@
1
+ import type { FinalArtifact, PostChatFollowup, ResultFeedback as SavedResultFeedback, ResultFeedbackReason } from "@blade-hq/agent-client";
2
+ export type FollowupInteractionEvent = {
3
+ type: "suggestions_shown";
4
+ sessionId?: string;
5
+ assistantEntryId: string;
6
+ count: number;
7
+ } | {
8
+ type: "suggestion_adopted";
9
+ sessionId?: string;
10
+ assistantEntryId: string;
11
+ suggestionIndex: number;
12
+ } | {
13
+ type: "artifact_shown";
14
+ sessionId?: string;
15
+ assistantEntryId: string;
16
+ artifactIndex: number;
17
+ artifactKind: FinalArtifact["kind"];
18
+ } | {
19
+ type: "artifact_opened";
20
+ sessionId?: string;
21
+ assistantEntryId: string;
22
+ artifactIndex: number;
23
+ artifactKind: FinalArtifact["kind"];
24
+ } | {
25
+ type: "artifact_download_started";
26
+ sessionId?: string;
27
+ assistantEntryId: string;
28
+ artifactIndex: number;
29
+ artifactKind: "file";
30
+ } | {
31
+ type: "artifact_download_succeeded";
32
+ sessionId?: string;
33
+ assistantEntryId: string;
34
+ artifactIndex: number;
35
+ artifactKind: "file";
36
+ } | {
37
+ type: "result_feedback_shown";
38
+ sessionId?: string;
39
+ assistantEntryId: string;
40
+ } | {
41
+ type: "result_feedback_submitted";
42
+ sessionId?: string;
43
+ assistantEntryId: string;
44
+ helpful: boolean;
45
+ reason: ResultFeedbackReason | null;
46
+ updatedAt: string;
47
+ };
48
+ export declare function HistoricalResultFeedback({ feedback }: {
49
+ feedback: SavedResultFeedback;
50
+ }): import("react/jsx-runtime").JSX.Element;
51
+ export declare function PostChatFollowupBlock({ followup, sessionId, onSuggestion, isViewer, onInteraction, savedFeedback, onFeedbackSaved, }: {
52
+ followup: PostChatFollowup;
53
+ sessionId?: string;
54
+ onSuggestion?: (value: string) => void;
55
+ isViewer?: boolean;
56
+ onInteraction?: (event: FollowupInteractionEvent) => void;
57
+ savedFeedback?: SavedResultFeedback;
58
+ onFeedbackSaved?: (feedback: SavedResultFeedback) => void;
59
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -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;
@@ -24,6 +24,8 @@ declare const BladeAgent: {
24
24
  SessionHub: typeof agentClient.SessionHub;
25
25
  SessionSetupError: typeof agentClient.SessionSetupError;
26
26
  createInitialSessionState: typeof agentClient.createInitialSessionState;
27
+ reconcileOptimisticUserTurns: typeof agentClient.reconcileOptimisticUserTurns;
28
+ toReplaySnapshot: typeof agentClient.toReplaySnapshot;
27
29
  connectEmbedded: typeof agentClient.connectEmbedded;
28
30
  isCommandEnvelope: typeof agentClient.isCommandEnvelope;
29
31
  isInboundEnvelope: typeof agentClient.isInboundEnvelope;
@@ -38,6 +40,8 @@ declare const BladeAgent: {
38
40
  isHiddenInternalMessage: typeof agentClient.isHiddenInternalMessage;
39
41
  normalizeMessageContent: typeof agentClient.normalizeMessageContent;
40
42
  transformSlashCommand: typeof agentClient.transformSlashCommand;
43
+ latestPostChatFollowup: typeof agentClient.latestPostChatFollowup;
44
+ DEFAULT_REPLAY_SPEED: agentClient.ReplaySpeed;
41
45
  SessionInfo: import("arktype/internal/variants/object.ts").ObjectType<{
42
46
  id: string;
43
47
  intent: string;
@@ -82,6 +86,7 @@ declare const BladeAgent: {
82
86
  number: string;
83
87
  }, {}>;
84
88
  TaskStatus: import("arktype/internal/variants/string.ts").StringType<"done" | "failed" | "in_progress" | "pending" | "skipped", {}>;
89
+ acceptedPostChatFollowupCompletesLatestRun: typeof agentClient.acceptedPostChatFollowupCompletesLatestRun;
85
90
  ClientProjectionBuilder: typeof agentClient.ClientProjectionBuilder;
86
91
  createSocket: typeof agentClient.createSocket;
87
92
  getChat: typeof getChat;