@blade-hq/agent-react 1.2.1-beta.12 → 1.2.1-beta.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ pnpm add @blade-hq/agent-client @blade-hq/agent-react
13
13
  ```tsx
14
14
  import { BladeClient } from "@blade-hq/agent-client"
15
15
  import { BladeProvider, ChatView } from "@blade-hq/agent-react"
16
- import "@blade-hq/agent-react/style.css"
16
+ import "@blade-hq/agent-react/style.full.css"
17
17
 
18
18
  const client = new BladeClient({ baseUrl: "https://blade.example.com" })
19
19
 
@@ -88,7 +88,11 @@ await session?.send("你好")
88
88
 
89
89
  全部 props 见 `ChatViewProps`。样式说明:
90
90
 
91
- - 必须引入 `@blade-hq/agent-react/style.css`。组件自带 `.blade-chat-*` 前缀的自包含样式,宿主没装 Tailwind 也开箱可用;颜色走 CSS 变量,可整体换肤/暗色。
91
+ - 必须引入一份样式,按宿主有没有 Tailwind 二选一:
92
+ - **`style.full.css`**(默认选它):布局兜底 + 编译好的 Tailwind 产物,自包含,宿主没装 Tailwind 也是完整视觉。
93
+ - **`style.css`**:只有 `.blade-chat-*` 布局兜底。宿主自己装了 Tailwind、且 `content` 配了扫描 `node_modules/@blade-hq/agent-react` 时用它,可以少一份重复 CSS。选错不会报错,只是工具块、思考块内部会退化成没有布局的裸 HTML。
94
+ - 两份都不含 Tailwind preflight 这类全局 reset,引入后不会影响宿主页面自己的排版;组件需要的那点重置限定在 `.blade-chat` 子树内(见 `src/scoped-preflight.css`)。Shadow DOM 版(`<blade-chat>`)不受此限,它注入的是带 preflight 的完整产物。
95
+ - 颜色走 CSS 变量,可整体换肤;深色页面要在根元素上写 `data-theme="dark"`,否则拿到的是默认的浅色变量。
92
96
  - `renderers.toolCall` 返回 `null` 时回落到默认工具卡片。
93
97
  - **内边距按容器宽度伸缩**,嵌进窄侧栏(300–400px)时会自动收窄,不看浏览器视口。代价是聊天容器需要有确定宽度——放在 flex/grid 里被拉伸,或显式给宽度都可以;放进 `width: fit-content`、`inline-flex` 这类由内容决定宽度的父容器会塌缩。
94
98
 
@@ -118,17 +122,19 @@ SDK 的 ChatView / `<blade-chat>` 不内置模型选择器(默认用后端配
118
122
 
119
123
  ## 样式定制
120
124
 
121
- 三个层次,按需选用:
125
+ 默认是浅色主题。四个层次,按需选用:
122
126
 
123
- 1. **CSS 变量换肤**(React 与 `<blade-chat>` 通用):所有颜色走 `--primary`、`--background`、`--muted` 等变量(完整清单见 style.css 开头)。页面直接覆盖即可穿透 Shadow DOM:
127
+ 1. **深浅主题**:`<ChatView theme="dark" />` / `<blade-chat theme="dark">` 切内置深色,不传或 `"light"` 为浅色。宿主已经在祖先元素上写了 `data-theme="dark"` 时不用传,`ChatView` 会跟着变。`<blade-chat>` 渲染在 Shadow DOM 内,页面上的 `data-theme` 对它无效,只认 `theme` 属性。
128
+
129
+ 2. **CSS 变量换肤**(React 与 `<blade-chat>` 通用):所有颜色走 `--primary`、`--background`、`--muted` 等变量(完整清单见 style.css 开头)。页面直接覆盖即可穿透 Shadow DOM:
124
130
 
125
131
  ```css
126
132
  blade-chat { --primary: 262 83% 58%; height: 640px; }
127
133
  ```
128
134
 
129
- 2. **classNames props**(React):`ChatView` 的 `classNames` 把自定义 class(含你自己构建的 Tailwind 工具类)挂到根节点、消息区、输入区等关键节点上,见 `ChatViewClassNames`。
135
+ 3. **classNames props**(React):`ChatView` 的 `classNames` 把自定义 class(含你自己构建的 Tailwind 工具类)挂到根节点、消息区、输入区等关键节点上,见 `ChatViewClassNames`。
130
136
 
131
- 3. **内嵌 `<style>`(纯 HTML / Vue)**:`<blade-chat>` 的直接 `<style>` 子元素会被注入 Shadow DOM、排在内置样式之后,可用 `.blade-chat-*` 类名做任意深度定制:
137
+ 4. **内嵌 `<style>`(纯 HTML / Vue)**:`<blade-chat>` 的直接 `<style>` 子元素会被注入 Shadow DOM、排在内置样式之后,可用 `.blade-chat-*` 类名做任意深度定制:
132
138
 
133
139
  ```html
134
140
  <blade-chat base-url="...">
@@ -33,6 +33,8 @@ export interface ChatViewProps {
33
33
  renderers?: ChatViewRenderers;
34
34
  slots?: ChatViewSlots;
35
35
  placeholder?: string;
36
+ /** 配色主题,默认浅色。宿主已经在祖先元素上写了 data-theme 时不用传。 */
37
+ theme?: "light" | "dark";
36
38
  }
37
39
  /**
38
40
  * 开箱即用的聊天界面:连接(或新建)一个会话,渲染消息流与输入框。
@@ -10,6 +10,23 @@ interface Props {
10
10
  sessionStatus?: string;
11
11
  renderer?: ToolCallRenderer;
12
12
  }
13
+ /**
14
+ * 判断提问卡片该等待作答还是展示成已作答,只认工具状态。
15
+ * 与 apps/web 的 ToolCallBlock 里同名函数保持一致:两处算出不同结果时,
16
+ * 同一份会话在内置页面能点、在 SDK 里点不动。
17
+ *
18
+ * 不要再拿 sessionStatus 兜底。历史投影算错时用 waiting_for_input 补救,
19
+ * 会同时把已经回答过的旧提问重新变成可作答(历史加载不会带回 answerData),
20
+ * 而且会盖住投影层的错误——这个 bug 就是这么藏住的。
21
+ */
22
+ export declare function resolveAskQuestionState({ toolStatus, hasAnswerData, fallbackAnswered, }: {
23
+ toolStatus: ToolCallInfo["status"];
24
+ hasAnswerData: boolean;
25
+ fallbackAnswered?: boolean;
26
+ }): {
27
+ awaitingAnswer: boolean;
28
+ answered: boolean;
29
+ };
13
30
  /**
14
31
  * 极简工具调用块:状态 + 展示名一行,点开后是参数/结果的 <pre> 折叠展示。
15
32
  * AskUserQuestion 特殊处理为提问选项卡片。
@@ -1,6 +1,6 @@
1
1
  import { type AgentSession } from "@blade-hq/agent-client";
2
2
  /**
3
- * `<blade-chat base-url="https://..." session-id="可选">`
3
+ * `<blade-chat base-url="https://..." session-id="可选" theme="light|dark">`
4
4
  *
5
5
  * 纯静态页面的聊天入口:Shadow DOM 内渲染完整 ChatView,
6
6
  * 未登录时组件自带登录引导,不传 session-id 自动建会话。
package/dist/index.js CHANGED
@@ -1690,6 +1690,17 @@ function normalizeOptionItem(value) {
1690
1690
 
1691
1691
  // src/components/ToolCallBlock.tsx
1692
1692
  import { Fragment, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1693
+ function resolveAskQuestionState({
1694
+ toolStatus,
1695
+ hasAnswerData,
1696
+ fallbackAnswered
1697
+ }) {
1698
+ const awaitingAnswer = !hasAnswerData && toolStatus === "awaiting_answer";
1699
+ return {
1700
+ awaitingAnswer,
1701
+ answered: hasAnswerData || !awaitingAnswer && (Boolean(fallbackAnswered) || toolStatus === "done" || toolStatus === "cancelled" || toolStatus === "error")
1702
+ };
1703
+ }
1693
1704
  function ToolCallBlock({
1694
1705
  toolCall,
1695
1706
  onAnswer,
@@ -1708,13 +1719,18 @@ function ToolCallBlock({
1708
1719
  }
1709
1720
  if (normalizedName === "AskUserQuestion") {
1710
1721
  const askData = parseAskUserQuestion(typeof toolCall.result === "string" ? toolCall.result : null) ?? parseAskUserQuestion(buildAskUserPayload(toolCall.arguments));
1711
- const canAnswer = toolCall.status === "awaiting_answer" && Boolean(onAnswer);
1722
+ const questionState = resolveAskQuestionState({
1723
+ toolStatus: toolCall.status,
1724
+ hasAnswerData: Boolean(answerData),
1725
+ fallbackAnswered: answered
1726
+ });
1727
+ const canAnswer = questionState.awaitingAnswer && Boolean(onAnswer);
1712
1728
  if (askData) {
1713
1729
  return /* @__PURE__ */ jsx9(
1714
1730
  AskUserQuestionBlock,
1715
1731
  {
1716
1732
  data: askData,
1717
- answered: toolCall.status === "awaiting_answer" ? false : Boolean(answered) || toolCall.status === "done",
1733
+ answered: questionState.answered,
1718
1734
  toolCallId: toolCall.id,
1719
1735
  sessionStatus: sessionStatus ?? "",
1720
1736
  answerData,
@@ -2207,6 +2223,9 @@ function PlanningDivider({ kind }) {
2207
2223
 
2208
2224
  // src/components/ChatView.tsx
2209
2225
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2226
+ function themeAttr(theme) {
2227
+ return theme === "dark" ? "dark" : void 0;
2228
+ }
2210
2229
  function isUnauthorizedError(error) {
2211
2230
  return error instanceof BladeApiError && error.status === 401;
2212
2231
  }
@@ -2247,16 +2266,23 @@ function ChatView(props) {
2247
2266
  const [attempt, setAttempt] = useState9(0);
2248
2267
  const [needLogin, setNeedLogin] = useState9(() => !client.hasToken());
2249
2268
  if (needLogin) {
2250
- return /* @__PURE__ */ jsx14("div", { className: cn("blade-chat flex min-h-0 flex-1 flex-col", props.classNames?.root), children: /* @__PURE__ */ jsx14(
2251
- LoginCard,
2269
+ return /* @__PURE__ */ jsx14(
2270
+ "div",
2252
2271
  {
2253
- client,
2254
- onLoggedIn: () => {
2255
- setNeedLogin(false);
2256
- setAttempt((value) => value + 1);
2257
- }
2272
+ "data-theme": themeAttr(props.theme),
2273
+ className: cn("blade-chat flex min-h-0 flex-1 flex-col", props.classNames?.root),
2274
+ children: /* @__PURE__ */ jsx14(
2275
+ LoginCard,
2276
+ {
2277
+ client,
2278
+ onLoggedIn: () => {
2279
+ setNeedLogin(false);
2280
+ setAttempt((value) => value + 1);
2281
+ }
2282
+ }
2283
+ )
2258
2284
  }
2259
- ) });
2285
+ );
2260
2286
  }
2261
2287
  return /* @__PURE__ */ jsx14(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
2262
2288
  }
@@ -2270,6 +2296,7 @@ function ChatSessionView({
2270
2296
  renderers,
2271
2297
  slots,
2272
2298
  placeholder,
2299
+ theme,
2273
2300
  onUnauthorized
2274
2301
  }) {
2275
2302
  const { session, state, error } = useAgentSession(sessionId, {
@@ -2328,6 +2355,7 @@ ${content}`);
2328
2355
  return /* @__PURE__ */ jsxs12(
2329
2356
  "div",
2330
2357
  {
2358
+ "data-theme": themeAttr(theme),
2331
2359
  className: cn(
2332
2360
  "blade-chat flex min-h-0 flex-1 flex-col overflow-hidden",
2333
2361
  classNames?.root
@@ -2340,7 +2368,7 @@ ${content}`);
2340
2368
  className: classNames?.banner
2341
2369
  }
2342
2370
  ),
2343
- errorMessage && /* @__PURE__ */ jsxs12("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 dark:border-red-800/30 dark:bg-red-950/30 dark:text-red-300", children: [
2371
+ errorMessage && /* @__PURE__ */ jsxs12("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
2344
2372
  /* @__PURE__ */ jsx14(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
2345
2373
  /* @__PURE__ */ jsx14("span", { children: errorMessage })
2346
2374
  ] }),