@blade-hq/agent-react 2610.0.0-beta.14 → 2610.0.0-beta.15

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
@@ -151,6 +151,7 @@ await replay.exitToAutonomous() // 退出回放,之后真的运
151
151
  全部 props 见 `ChatViewProps`。样式说明:
152
152
 
153
153
  `onFollowupInteraction` 使用 `FollowupInteractionEvent`,覆盖下一步建议展示/采纳、成果展示/打开/下载以及结果评分。SDK 不内置 PostHog 等分析厂商;宿主回调抛错也不会中断用户点击或下载。
154
+ 文件成果以文件名为文本的标准下载链接展示;Vue / 纯 HTML 使用的 `<blade-chat>` 与 `ChatView` 行为一致。
154
155
 
155
156
  - 必须引入一份样式,按宿主有没有 Tailwind 二选一:
156
157
  - **`style.full.css`**(默认选它):布局兜底 + 编译好的 Tailwind 产物,自包含,宿主没装 Tailwind 也是完整视觉。
@@ -18,6 +18,9 @@ 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;
21
24
  SDK_NAME: "agent-client";
22
25
  SDK_VERSION: string;
23
26
  AgentSession: typeof AgentSession;
package/dist/index.js CHANGED
@@ -697,13 +697,6 @@ var Copy = createLucideIcon("Copy", [
697
697
  ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2", key: "zix9uf" }]
698
698
  ]);
699
699
 
700
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/download.js
701
- var Download = createLucideIcon("Download", [
702
- ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }],
703
- ["polyline", { points: "7 10 12 15 17 10", key: "2ggqvy" }],
704
- ["line", { x1: "12", x2: "12", y1: "15", y2: "3", key: "1vk2je" }]
705
- ]);
706
-
707
700
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-text.js
708
701
  var FileText = createLucideIcon("FileText", [
709
702
  ["path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z", key: "1rqfz7" }],
@@ -713,24 +706,6 @@ var FileText = createLucideIcon("FileText", [
713
706
  ["path", { d: "M16 17H8", key: "z1uh3a" }]
714
707
  ]);
715
708
 
716
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file.js
717
- var File = createLucideIcon("File", [
718
- ["path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z", key: "1rqfz7" }],
719
- ["path", { d: "M14 2v4a2 2 0 0 0 2 2h4", key: "tnqrlb" }]
720
- ]);
721
-
722
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/film.js
723
- var Film = createLucideIcon("Film", [
724
- ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }],
725
- ["path", { d: "M7 3v18", key: "bbkbws" }],
726
- ["path", { d: "M3 7.5h4", key: "zfgn84" }],
727
- ["path", { d: "M3 12h18", key: "1i2n21" }],
728
- ["path", { d: "M3 16.5h4", key: "1230mu" }],
729
- ["path", { d: "M17 3v18", key: "in4fa5" }],
730
- ["path", { d: "M17 7.5h4", key: "myr1c1" }],
731
- ["path", { d: "M17 16.5h4", key: "go4c1d" }]
732
- ]);
733
-
734
709
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/globe.js
735
710
  var Globe = createLucideIcon("Globe", [
736
711
  ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
@@ -1128,7 +1103,7 @@ function ConnectionBanner({ connection, className }) {
1128
1103
 
1129
1104
  // src/components/MessageList.tsx
1130
1105
  import { isHiddenInternalMessage } from "@blade-hq/agent-client";
1131
- import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo7, useRef as useRef8, useState as useState12 } from "react";
1106
+ import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo7, useRef as useRef9, useState as useState12 } from "react";
1132
1107
 
1133
1108
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/useStickToBottom.js
1134
1109
  import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
@@ -1841,8 +1816,37 @@ function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
1841
1816
  import { useState as useState9 } from "react";
1842
1817
 
1843
1818
  // src/components/AskUserQuestionBlock.tsx
1844
- import { useEffect as useEffect6, useMemo as useMemo6, useState as useState8 } from "react";
1819
+ import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
1845
1820
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1821
+ var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
1822
+ function resizeCustomTextarea(textarea) {
1823
+ textarea.style.height = "auto";
1824
+ textarea.style.height = `${Math.min(textarea.scrollHeight, CUSTOM_TEXTAREA_MAX_HEIGHT)}px`;
1825
+ textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
1826
+ }
1827
+ function useAutoResizeTextarea(value) {
1828
+ const textareaRef = useRef7(null);
1829
+ useEffect6(() => {
1830
+ const textarea = textareaRef.current;
1831
+ if (textarea?.value === value) resizeCustomTextarea(textarea);
1832
+ }, [value]);
1833
+ useEffect6(() => {
1834
+ const textarea = textareaRef.current;
1835
+ if (!textarea || typeof ResizeObserver === "undefined") return;
1836
+ let previousWidth = textarea.clientWidth;
1837
+ const observer = new ResizeObserver(([entry]) => {
1838
+ if (!entry || entry.contentRect.width === previousWidth) return;
1839
+ previousWidth = entry.contentRect.width;
1840
+ resizeCustomTextarea(textarea);
1841
+ });
1842
+ observer.observe(textarea);
1843
+ return () => observer.disconnect();
1844
+ }, []);
1845
+ return textareaRef;
1846
+ }
1847
+ function indentAnswerContinuationLines(answer) {
1848
+ return answer.replaceAll("\n", "\n ");
1849
+ }
1846
1850
  function AskUserQuestionBlock({
1847
1851
  data,
1848
1852
  answered,
@@ -1939,7 +1943,9 @@ function AskUserQuestionBlock({
1939
1943
  Array.from(usingCustom).map((qIdx) => [qIdx, (customTexts.get(qIdx) ?? "").trim()]).filter(([, text2]) => text2.length > 0)
1940
1944
  )
1941
1945
  };
1942
- const parts = data.questions.map((q, i) => `- ${q.question} -> ${getAnswer(i)}`);
1946
+ const parts = data.questions.map(
1947
+ (q, i) => `- ${q.question} -> ${indentAnswerContinuationLines(getAnswer(i) ?? "")}`
1948
+ );
1943
1949
  const text = `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
1944
1950
  ${parts.join("\n")}`;
1945
1951
  setSubmitted(true);
@@ -2011,6 +2017,7 @@ function QuestionCard({
2011
2017
  onCustomChange
2012
2018
  }) {
2013
2019
  const multi = question.multiSelect ?? false;
2020
+ const customTextareaRef = useAutoResizeTextarea(customText);
2014
2021
  return /* @__PURE__ */ jsxs7("div", { children: [
2015
2022
  /* @__PURE__ */ jsxs7("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
2016
2023
  /* @__PURE__ */ jsx9(
@@ -2087,25 +2094,26 @@ function QuestionCard({
2087
2094
  "div",
2088
2095
  {
2089
2096
  className: cn(
2090
- "flex items-center gap-2 rounded-lg border transition-all",
2097
+ "flex items-start gap-2 rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2091
2098
  answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2092
2099
  isCustom ? "border-[hsl(var(--ring)/0.6)] bg-[hsl(var(--accent))]" : "border-[hsl(var(--border))] hover:border-[hsl(var(--ring)/0.3)] hover:bg-[hsl(var(--accent))]",
2093
2100
  answered && "cursor-default opacity-70"
2094
2101
  ),
2095
2102
  children: [
2096
- /* @__PURE__ */ jsx9("span", { className: "shrink-0 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2103
+ /* @__PURE__ */ jsx9("span", { className: "shrink-0 pt-1 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2097
2104
  /* @__PURE__ */ jsx9(
2098
- "input",
2105
+ "textarea",
2099
2106
  {
2100
- type: "text",
2107
+ ref: customTextareaRef,
2108
+ rows: 2,
2101
2109
  value: customText,
2102
- disabled: answered,
2110
+ readOnly: answered,
2103
2111
  onChange: (e) => onCustomChange(qIdx, e.target.value),
2104
2112
  onFocus: () => onCustomFocus(qIdx),
2105
2113
  "aria-label": "\u81EA\u5B9A\u4E49\u56DE\u7B54",
2106
2114
  placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848...",
2107
2115
  className: cn(
2108
- "min-w-0 flex-1 bg-transparent text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2116
+ "min-h-10 min-w-0 flex-1 resize-none bg-transparent leading-5 text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2109
2117
  answered ? "text-xs" : "text-sm"
2110
2118
  )
2111
2119
  }
@@ -2436,7 +2444,7 @@ var RenderErrorBoundary = class extends Component {
2436
2444
  };
2437
2445
 
2438
2446
  // src/components/PostChatFollowupBlock.tsx
2439
- import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef7, useState as useState11 } from "react";
2447
+ import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef8, useState as useState11 } from "react";
2440
2448
  import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2441
2449
  function emitInteraction(callback, event) {
2442
2450
  try {
@@ -2447,9 +2455,6 @@ function emitInteraction(callback, event) {
2447
2455
  function basename(path) {
2448
2456
  return path.split(/[\\/]/).filter(Boolean).pop() || path;
2449
2457
  }
2450
- function isVideo(path) {
2451
- return /\.(?:mp4|mov|webm|mkv|avi|m4v)$/i.test(path);
2452
- }
2453
2458
  function ArtifactCard({
2454
2459
  artifact,
2455
2460
  sessionId,
@@ -2480,44 +2485,48 @@ ${artifact.target}`,
2480
2485
  }
2481
2486
  );
2482
2487
  }
2483
- const Icon2 = isVideo(artifact.target) ? Film : File;
2484
- return /* @__PURE__ */ jsxs11(
2485
- "button",
2488
+ const fileName = basename(artifact.target);
2489
+ const downloadUrl = sessionId ? client.buildAuthedUrl(
2490
+ `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(artifact.target)}`
2491
+ ) : void 0;
2492
+ const handleDownload = async (event) => {
2493
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
2494
+ event.preventDefault();
2495
+ if (!sessionId || downloading) return;
2496
+ setDownloading(true);
2497
+ emitInteraction(onInteraction, {
2498
+ type: "artifact_download_started",
2499
+ sessionId,
2500
+ assistantEntryId,
2501
+ artifactIndex,
2502
+ artifactKind: "file"
2503
+ });
2504
+ try {
2505
+ await client.sessions.downloadFile(sessionId, artifact.target, fileName);
2506
+ emitInteraction(onInteraction, {
2507
+ type: "artifact_download_succeeded",
2508
+ sessionId,
2509
+ assistantEntryId,
2510
+ artifactIndex,
2511
+ artifactKind: "file"
2512
+ });
2513
+ } catch {
2514
+ } finally {
2515
+ setDownloading(false);
2516
+ }
2517
+ };
2518
+ return /* @__PURE__ */ jsx13(
2519
+ "a",
2486
2520
  {
2487
- type: "button",
2488
- disabled: !sessionId || downloading,
2489
- onClick: async () => {
2490
- if (!sessionId || downloading) return;
2491
- setDownloading(true);
2492
- emitInteraction(onInteraction, {
2493
- type: "artifact_download_started",
2494
- sessionId,
2495
- assistantEntryId,
2496
- artifactIndex,
2497
- artifactKind: "file"
2498
- });
2499
- try {
2500
- await client.sessions.downloadFile(sessionId, artifact.target, basename(artifact.target));
2501
- emitInteraction(onInteraction, {
2502
- type: "artifact_download_succeeded",
2503
- sessionId,
2504
- assistantEntryId,
2505
- artifactIndex,
2506
- artifactKind: "file"
2507
- });
2508
- } catch {
2509
- } finally {
2510
- setDownloading(false);
2511
- }
2512
- },
2513
- title: name,
2514
- "aria-label": `\u4E0B\u8F7D ${name}`,
2515
- className: "group relative flex min-w-0 items-center gap-1.5 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-2 py-1.5 text-left text-xs text-[hsl(var(--card-foreground))] hover:bg-[hsl(var(--accent))] disabled:opacity-60",
2516
- children: [
2517
- /* @__PURE__ */ jsx13(Icon2, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
2518
- /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
2519
- /* @__PURE__ */ jsx13(Download, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
2520
- ]
2521
+ href: downloadUrl,
2522
+ download: fileName,
2523
+ onClick: handleDownload,
2524
+ title: fileName,
2525
+ "aria-label": `\u4E0B\u8F7D\u6587\u4EF6\uFF1A${fileName}`,
2526
+ "aria-disabled": !sessionId || void 0,
2527
+ "aria-busy": downloading || void 0,
2528
+ className: "min-w-0 cursor-pointer break-all text-xs text-[hsl(var(--primary))] underline aria-disabled:cursor-not-allowed aria-disabled:opacity-60 aria-busy:cursor-wait",
2529
+ children: fileName
2521
2530
  }
2522
2531
  );
2523
2532
  }
@@ -2566,8 +2575,8 @@ function ResultFeedback({
2566
2575
  const [reason, setReason] = useState11(savedFeedback?.reason ?? null);
2567
2576
  const [saving, setSaving] = useState11(false);
2568
2577
  const [saveError, setSaveError] = useState11(false);
2569
- const reportedShown = useRef7(false);
2570
- const latestChoice = useRef7(null);
2578
+ const reportedShown = useRef8(false);
2579
+ const latestChoice = useRef8(null);
2571
2580
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
2572
2581
  useEffect7(() => {
2573
2582
  if (!eligible || reportedShown.current) return;
@@ -2691,10 +2700,10 @@ function PostChatFollowupBlock({
2691
2700
  onFeedbackSaved
2692
2701
  }) {
2693
2702
  const [expanded, setExpanded] = useState11(false);
2694
- const adopted = useRef7(/* @__PURE__ */ new Set());
2695
- const reportedSuggestions = useRef7(false);
2696
- const reportedArtifacts = useRef7(/* @__PURE__ */ new Set());
2697
- const openedArtifacts = useRef7(/* @__PURE__ */ new Set());
2703
+ const adopted = useRef8(/* @__PURE__ */ new Set());
2704
+ const reportedSuggestions = useRef8(false);
2705
+ const reportedArtifacts = useRef8(/* @__PURE__ */ new Set());
2706
+ const openedArtifacts = useRef8(/* @__PURE__ */ new Set());
2698
2707
  const artifacts = followup.final_artifacts ?? [];
2699
2708
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
2700
2709
  useEffect7(() => {
@@ -3071,7 +3080,7 @@ function MessageList({
3071
3080
  }
3072
3081
  function AutoScrollOnUserSend({ userMessageCount }) {
3073
3082
  const { scrollToBottom } = useStickToBottomContext();
3074
- const previousCountRef = useRef8(userMessageCount);
3083
+ const previousCountRef = useRef9(userMessageCount);
3075
3084
  useEffect8(() => {
3076
3085
  if (userMessageCount > previousCountRef.current) {
3077
3086
  scrollToBottom("instant");
@@ -3083,7 +3092,7 @@ function AutoScrollOnUserSend({ userMessageCount }) {
3083
3092
  function ScrollToBottomButton() {
3084
3093
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
3085
3094
  const [visible, setVisible] = useState12(false);
3086
- const hideTimerRef = useRef8(null);
3095
+ const hideTimerRef = useRef9(null);
3087
3096
  useEffect8(() => {
3088
3097
  if (isAtBottom) {
3089
3098
  if (!hideTimerRef.current) {
@@ -3696,10 +3705,7 @@ lucide-react/dist/esm/icons/chevron-down.js:
3696
3705
  lucide-react/dist/esm/icons/chevron-right.js:
3697
3706
  lucide-react/dist/esm/icons/circle-alert.js:
3698
3707
  lucide-react/dist/esm/icons/copy.js:
3699
- lucide-react/dist/esm/icons/download.js:
3700
3708
  lucide-react/dist/esm/icons/file-text.js:
3701
- lucide-react/dist/esm/icons/file.js:
3702
- lucide-react/dist/esm/icons/film.js:
3703
3709
  lucide-react/dist/esm/icons/globe.js:
3704
3710
  lucide-react/dist/esm/icons/layers.js:
3705
3711
  lucide-react/dist/esm/icons/lightbulb.js: