@fv-ui/ai-chat 0.8.0 → 0.9.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/dist/index.d.ts CHANGED
@@ -34,6 +34,8 @@ type __VLS_Props$g = {
34
34
  bgColor?: string;
35
35
  width?: string;
36
36
  };
37
+ /** typing 对象形态的 interval 语义:自适应模式下的"基础节拍"(每拍最小步进 1 字的期望速率),
38
+ * 实际步进随积压放大(见 startTyping 内注释) */
37
39
  typing?: boolean | {
38
40
  step?: number;
39
41
  interval?: number;
@@ -1484,7 +1486,7 @@ type __VLS_Props$3 = {
1484
1486
  suggestions?: AiMessagesSuggestion[];
1485
1487
  /** 空态建议区标题 */
1486
1488
  suggestionsTitle?: string;
1487
- /** 用户上滚免打扰阈值(px):距底不超过该值才自动吸底 */
1489
+ /** 用户上滚免打扰阈值(px):距底不超过该值视为「钉在底部」,流式/完成自动跟随 */
1488
1490
  scrollThreshold?: number;
1489
1491
  /** 显示滚到底部悬浮按钮 */
1490
1492
  showScrollButton?: boolean;
@@ -1510,6 +1512,11 @@ type __VLS_Props$3 = {
1510
1512
  userName?: string;
1511
1513
  /** 助手名称(透传 AiMessage) */
1512
1514
  assistantName?: string;
1515
+ /** 打字机(透传 AiMessage;仅 assistant 流式期间生效,落定/历史消息整段渲染) */
1516
+ typing?: boolean | {
1517
+ step?: number;
1518
+ interval?: number;
1519
+ };
1513
1520
  };
1514
1521
  declare function scrollToBottom(smooth?: boolean): void;
1515
1522
  /** 回到顶部:与 scrollToBottom 同款 try/catch 回落(happy-dom 旧版 scrollTo(options) 可能缺实现) */
@@ -1568,6 +1575,10 @@ declare const __VLS_base$2: vue.DefineComponent<__VLS_Props$3, {
1568
1575
  "onBranch-change"?: ((dir: BranchDirection, message: MessageNode) => any) | undefined;
1569
1576
  }>, {
1570
1577
  name: string;
1578
+ typing: boolean | {
1579
+ step?: number;
1580
+ interval?: number;
1581
+ };
1571
1582
  icon: string | Component;
1572
1583
  nodes: Record<string, MessageNode>;
1573
1584
  description: string;
@@ -1650,6 +1661,12 @@ type __VLS_Props$2 = {
1650
1661
  placeholder?: string;
1651
1662
  /** 工具名 → 步骤展示名(透传 useChat toolStepLabel,流式工具轨迹步骤的 title) */
1652
1663
  toolStepLabel?: (toolName: string) => string;
1664
+ /** 打字机(透传 AiMessages;仅 assistant 流式期间逐字平滑打出,
1665
+ * 落定/历史消息整段渲染;true 用默认节奏,对象形态自定义 step/interval) */
1666
+ typing?: boolean | {
1667
+ step?: number;
1668
+ interval?: number;
1669
+ };
1653
1670
  /** 输入区阴影效果(透传 Sender;关闭传 false) */
1654
1671
  senderShadow?: boolean;
1655
1672
  /** 空态建议列表 */
@@ -1752,6 +1769,10 @@ declare const __VLS_base$1: vue.DefineComponent<__VLS_Props$2, {
1752
1769
  }) => any) | undefined;
1753
1770
  }>, {
1754
1771
  title: string;
1772
+ typing: boolean | {
1773
+ step?: number;
1774
+ interval?: number;
1775
+ };
1755
1776
  placeholder: string;
1756
1777
  accept: string;
1757
1778
  baseURL: string;
@@ -1823,6 +1844,11 @@ type __VLS_Props$1 = {
1823
1844
  assistantName?: string;
1824
1845
  /** Thinking autoCollapse 透传 */
1825
1846
  autoCollapse?: boolean;
1847
+ /** 打字机(透传 Typewriter;仅 assistant 流式期间生效,落定/历史消息整段渲染) */
1848
+ typing?: boolean | {
1849
+ step?: number;
1850
+ interval?: number;
1851
+ };
1826
1852
  };
1827
1853
  declare var __VLS_1: {
1828
1854
  message: MessageNode;
@@ -1839,7 +1865,7 @@ declare var __VLS_33: {
1839
1865
  message: MessageNode;
1840
1866
  files: ChatFile[];
1841
1867
  };
1842
- declare var __VLS_57: {
1868
+ declare var __VLS_67: {
1843
1869
  message: MessageNode;
1844
1870
  };
1845
1871
  type __VLS_Slots = {} & {
@@ -1855,7 +1881,7 @@ type __VLS_Slots = {} & {
1855
1881
  } & {
1856
1882
  'message-files'?: (props: typeof __VLS_33) => any;
1857
1883
  } & {
1858
- 'message-actions'?: (props: typeof __VLS_57) => any;
1884
+ 'message-actions'?: (props: typeof __VLS_67) => any;
1859
1885
  };
1860
1886
  declare const __VLS_base: vue.DefineComponent<__VLS_Props$1, {
1861
1887
  message: vue.ComputedRef<MessageNode>;
@@ -1868,6 +1894,10 @@ declare const __VLS_base: vue.DefineComponent<__VLS_Props$1, {
1868
1894
  onRegenerate?: ((message: MessageNode) => any) | undefined;
1869
1895
  "onBranch-change"?: ((dir: BranchDirection, message: MessageNode) => any) | undefined;
1870
1896
  }>, {
1897
+ typing: boolean | {
1898
+ step?: number;
1899
+ interval?: number;
1900
+ };
1871
1901
  autoCollapse: boolean;
1872
1902
  branch: AiMessageBranchInfo;
1873
1903
  showActions: boolean;
package/es/index.mjs CHANGED
@@ -1595,7 +1595,9 @@ var _sfc_main$h = /* @__PURE__ */ defineComponent({
1595
1595
  const step = (typeof props.typing === "object" ? props.typing.step : void 0) ?? 2;
1596
1596
  const interval = (typeof props.typing === "object" ? props.typing.interval : void 0) ?? 50;
1597
1597
  timer = setInterval(() => {
1598
- cursor = Math.min(props.content.length, cursor + step);
1598
+ const backlog = props.content.length - cursor;
1599
+ const speed = Math.min(step * 6, Math.max(1, Math.round(step * (1 + backlog / 8))));
1600
+ cursor = Math.min(props.content.length, cursor + speed);
1599
1601
  renderedContent.value = props.content.slice(0, cursor);
1600
1602
  progress.value = props.content.length ? cursor / props.content.length : 1;
1601
1603
  emit("writing");
@@ -6049,7 +6051,7 @@ const _hoisted_12 = {
6049
6051
  class: "fv-msg-error"
6050
6052
  };
6051
6053
  const _hoisted_13 = {
6052
- key: 2,
6054
+ key: 3,
6053
6055
  class: "fv-msg-pending"
6054
6056
  };
6055
6057
  const _hoisted_14 = {
@@ -6081,7 +6083,8 @@ var _sfc_main$3 = /* @__PURE__ */ defineComponent({
6081
6083
  assistantAvatar: { type: String, required: false, default: "" },
6082
6084
  userName: { type: String, required: false, default: "" },
6083
6085
  assistantName: { type: String, required: false, default: "" },
6084
- autoCollapse: { type: Boolean, required: false, default: true }
6086
+ autoCollapse: { type: Boolean, required: false, default: true },
6087
+ typing: { type: [Boolean, Object], required: false, default: false }
6085
6088
  },
6086
6089
  emits: ["copy", "regenerate", "branch-change"],
6087
6090
  setup(__props, { expose: __expose, emit: __emit }) {
@@ -6096,6 +6099,9 @@ var _sfc_main$3 = /* @__PURE__ */ defineComponent({
6096
6099
  });
6097
6100
  const hasReasoning = computed(() => !!props.message.reasoning);
6098
6101
  const hasToolSteps = computed(() => (props.message.toolSteps?.length ?? 0) > 0);
6102
+ const useTypewriter = computed(
6103
+ () => !isUser.value && !!props.typing && (props.message.status === "pending" || props.message.status === "streaming")
6104
+ );
6099
6105
  const toolChainItems = computed(() => {
6100
6106
  const steps = props.message.toolSteps ?? [];
6101
6107
  const running = props.message.status === "pending" || props.message.status === "streaming";
@@ -6231,7 +6237,7 @@ var _sfc_main$3 = /* @__PURE__ */ defineComponent({
6231
6237
  class: "fv-msg-tools",
6232
6238
  items: toolChainItems.value
6233
6239
  }, null, 8, ["items"])) : createCommentVNode("v-if", true),
6234
- createCommentVNode(" \u601D\u8003\u94FE\u6298\u53E0(assistant \u542B reasoning \u65F6) "),
6240
+ createCommentVNode(" \u601D\u8003\u94FE\u6298\u53E0(assistant \u542B reasoning \u65F6);\u6D41\u5F0F\u671F\u95F4\u7ECF\u6253\u5B57\u673A\u5E73\u6ED1\u6253\u51FA "),
6235
6241
  !isUser.value && hasReasoning.value ? (openBlock(), createBlock(_sfc_main$a, {
6236
6242
  key: 3,
6237
6243
  content: __props.message.reasoning,
@@ -6239,9 +6245,15 @@ var _sfc_main$3 = /* @__PURE__ */ defineComponent({
6239
6245
  "auto-collapse": __props.autoCollapse
6240
6246
  }, {
6241
6247
  content: withCtx(() => [
6242
- createVNode(_sfc_main$i, {
6248
+ useTypewriter.value ? (openBlock(), createBlock(_sfc_main$h, {
6249
+ key: 0,
6250
+ content: __props.message.reasoning ?? "",
6251
+ "is-markdown": "",
6252
+ typing: __props.typing
6253
+ }, null, 8, ["content", "typing"])) : (openBlock(), createBlock(_sfc_main$i, {
6254
+ key: 1,
6243
6255
  content: __props.message.reasoning ?? ""
6244
- }, null, 8, ["content"])
6256
+ }, null, 8, ["content"]))
6245
6257
  ]),
6246
6258
  _: 1
6247
6259
  /* STABLE */
@@ -6254,8 +6266,13 @@ var _sfc_main$3 = /* @__PURE__ */ defineComponent({
6254
6266
  toDisplayString(__props.message.error || "\u751F\u6210\u5931\u8D25"),
6255
6267
  1
6256
6268
  /* TEXT */
6257
- )) : __props.message.content ? (openBlock(), createBlock(_sfc_main$i, {
6269
+ )) : useTypewriter.value && __props.message.content ? (openBlock(), createBlock(_sfc_main$h, {
6258
6270
  key: 1,
6271
+ content: __props.message.content,
6272
+ "is-markdown": "",
6273
+ typing: __props.typing
6274
+ }, null, 8, ["content", "typing"])) : __props.message.content ? (openBlock(), createBlock(_sfc_main$i, {
6275
+ key: 2,
6259
6276
  content: __props.message.content
6260
6277
  }, null, 8, ["content"])) : __props.message.status === "pending" || __props.message.status === "streaming" ? (openBlock(), createElementBlock("span", _hoisted_13, [
6261
6278
  (openBlock(), createElementBlock(
@@ -6450,7 +6467,7 @@ var _sfc_main$2 = /* @__PURE__ */ defineComponent({
6450
6467
  description: { type: String, required: false, default: "" },
6451
6468
  suggestions: { type: Array, required: false, default: () => [] },
6452
6469
  suggestionsTitle: { type: String, required: false, default: "" },
6453
- scrollThreshold: { type: Number, required: false, default: 40 },
6470
+ scrollThreshold: { type: Number, required: false, default: 100 },
6454
6471
  showScrollButton: { type: Boolean, required: false, default: true },
6455
6472
  scrollButtonThreshold: { type: Number, required: false, default: 100 },
6456
6473
  showActions: { type: Boolean, required: false, default: true },
@@ -6462,7 +6479,8 @@ var _sfc_main$2 = /* @__PURE__ */ defineComponent({
6462
6479
  userAvatar: { type: String, required: false, default: "" },
6463
6480
  assistantAvatar: { type: String, required: false, default: "" },
6464
6481
  userName: { type: String, required: false, default: "" },
6465
- assistantName: { type: String, required: false, default: "" }
6482
+ assistantName: { type: String, required: false, default: "" },
6483
+ typing: { type: [Boolean, Object], required: false, default: false }
6466
6484
  },
6467
6485
  emits: ["select", "copy", "regenerate", "branch-change"],
6468
6486
  setup(__props, { expose: __expose, emit: __emit }) {
@@ -6475,21 +6493,35 @@ var _sfc_main$2 = /* @__PURE__ */ defineComponent({
6475
6493
  if (!el) return 0;
6476
6494
  return el.scrollHeight - el.scrollTop - el.clientHeight;
6477
6495
  }
6496
+ const isPinnedToBottom = ref(true);
6478
6497
  function scrollToBottom(smooth = true) {
6479
6498
  const el = scrollRef.value;
6480
6499
  if (!el) return;
6500
+ isPinnedToBottom.value = true;
6481
6501
  try {
6482
6502
  el.scrollTo({ top: el.scrollHeight, behavior: smooth ? "smooth" : "auto" });
6483
6503
  } catch {
6484
6504
  el.scrollTop = el.scrollHeight;
6485
6505
  }
6486
6506
  }
6487
- function scrollToBottomIfNear() {
6488
- if (distanceFromBottom() <= props.scrollThreshold) nextTick(() => scrollToBottom());
6507
+ function scrollToBottomNow() {
6508
+ const el = scrollRef.value;
6509
+ if (!el) return;
6510
+ isPinnedToBottom.value = true;
6511
+ try {
6512
+ el.scrollTo({ top: el.scrollHeight, behavior: "auto" });
6513
+ } catch {
6514
+ el.scrollTop = el.scrollHeight;
6515
+ }
6516
+ }
6517
+ function followIfPinned() {
6518
+ if (isPinnedToBottom.value) nextTick(() => scrollToBottomNow());
6489
6519
  }
6490
6520
  const isAwayFromBottom = ref(false);
6491
6521
  function onScroll() {
6492
- isAwayFromBottom.value = distanceFromBottom() > props.scrollButtonThreshold;
6522
+ const d = distanceFromBottom();
6523
+ isPinnedToBottom.value = d <= props.scrollThreshold;
6524
+ isAwayFromBottom.value = d > props.scrollButtonThreshold;
6493
6525
  }
6494
6526
  watch(() => props.scrollButtonThreshold, onScroll);
6495
6527
  onMounted(() => {
@@ -6497,7 +6529,7 @@ var _sfc_main$2 = /* @__PURE__ */ defineComponent({
6497
6529
  onScroll();
6498
6530
  });
6499
6531
  watch(() => props.messages.length, (nv, ov) => {
6500
- if (nv > (ov ?? 0)) scrollToBottomIfNear();
6532
+ if (nv > (ov ?? 0)) followIfPinned();
6501
6533
  });
6502
6534
  watch(
6503
6535
  () => props.messages.filter((m) => m.role === "user").length,
@@ -6510,11 +6542,11 @@ var _sfc_main$2 = /* @__PURE__ */ defineComponent({
6510
6542
  (sum, m) => sum + m.content.length + (m.reasoning?.length ?? 0) + (m.toolSteps?.reduce((s, t) => s + (t.status?.length ?? 0) + (t.title?.length ?? 0), 0) ?? 0) + (m.toolSteps?.length ?? 0),
6511
6543
  0
6512
6544
  ),
6513
- () => scrollToBottomIfNear()
6545
+ () => followIfPinned()
6514
6546
  );
6515
6547
  watch(
6516
6548
  () => props.messages.reduce((sum, m) => sum + (m.status === "done" || m.status === "error" ? 1 : 0), 0),
6517
- () => scrollToBottomIfNear()
6549
+ () => followIfPinned()
6518
6550
  );
6519
6551
  function branchInfo(m) {
6520
6552
  if (!m) return void 0;
@@ -6601,6 +6633,7 @@ var _sfc_main$2 = /* @__PURE__ */ defineComponent({
6601
6633
  "assistant-avatar": __props.assistantAvatar,
6602
6634
  "user-name": __props.userName,
6603
6635
  "assistant-name": __props.assistantName,
6636
+ typing: __props.typing,
6604
6637
  onCopy,
6605
6638
  onRegenerate,
6606
6639
  onBranchChange: onBranch
@@ -6650,7 +6683,7 @@ var _sfc_main$2 = /* @__PURE__ */ defineComponent({
6650
6683
  ]),
6651
6684
  key: "5"
6652
6685
  } : void 0
6653
- ]), 1032, ["message", "branch", "show-actions", "show-branch-switch", "show-user-avatar", "show-user-name", "show-assistant-avatar", "show-assistant-name", "user-avatar", "assistant-avatar", "user-name", "assistant-name"]);
6686
+ ]), 1032, ["message", "branch", "show-actions", "show-branch-switch", "show-user-avatar", "show-user-name", "show-assistant-avatar", "show-assistant-name", "user-avatar", "assistant-avatar", "user-name", "assistant-name", "typing"]);
6654
6687
  }),
6655
6688
  128
6656
6689
  /* KEYED_FRAGMENT */
@@ -6988,6 +7021,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
6988
7021
  assistantName: { type: String, required: false, default: "" },
6989
7022
  placeholder: { type: String, required: false, default: "" },
6990
7023
  toolStepLabel: { type: Function, required: false, default: void 0 },
7024
+ typing: { type: [Boolean, Object], required: false, default: false },
6991
7025
  senderShadow: { type: Boolean, required: false, default: true },
6992
7026
  suggestions: { type: Array, required: false, default: () => [] },
6993
7027
  features: { type: Object, required: false, default: () => ({}) },
@@ -7261,6 +7295,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
7261
7295
  "assistant-avatar": __props.assistantAvatar,
7262
7296
  "user-name": __props.userName,
7263
7297
  "assistant-name": __props.assistantName,
7298
+ typing: __props.typing,
7264
7299
  onSelect: onSelectSuggestion,
7265
7300
  onCopy,
7266
7301
  onRegenerate,
@@ -7332,7 +7367,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
7332
7367
  ]),
7333
7368
  key: "8"
7334
7369
  } : void 0
7335
- ]), 1032, ["messages", "nodes", "name", "suggestions", "show-scroll-button", "scroll-button-threshold", "show-user-avatar", "show-user-name", "show-assistant-avatar", "show-assistant-name", "user-avatar", "assistant-avatar", "user-name", "assistant-name"]),
7370
+ ]), 1032, ["messages", "nodes", "name", "suggestions", "show-scroll-button", "scroll-button-threshold", "show-user-avatar", "show-user-name", "show-assistant-avatar", "show-assistant-name", "user-avatar", "assistant-avatar", "user-name", "assistant-name", "typing"]),
7336
7371
  __props.features?.artifacts && artifacts.value.length ? (openBlock(), createBlock(_sfc_main$1, {
7337
7372
  key: 0,
7338
7373
  artifacts: artifacts.value,
package/lib/index.js CHANGED
@@ -1611,7 +1611,9 @@
1611
1611
  const step = (typeof props.typing === "object" ? props.typing.step : void 0) ?? 2;
1612
1612
  const interval = (typeof props.typing === "object" ? props.typing.interval : void 0) ?? 50;
1613
1613
  timer = setInterval(() => {
1614
- cursor = Math.min(props.content.length, cursor + step);
1614
+ const backlog = props.content.length - cursor;
1615
+ const speed = Math.min(step * 6, Math.max(1, Math.round(step * (1 + backlog / 8))));
1616
+ cursor = Math.min(props.content.length, cursor + speed);
1615
1617
  renderedContent.value = props.content.slice(0, cursor);
1616
1618
  progress.value = props.content.length ? cursor / props.content.length : 1;
1617
1619
  emit("writing");
@@ -6065,7 +6067,7 @@ ${fileParts.join("\n")}` });
6065
6067
  class: "fv-msg-error"
6066
6068
  };
6067
6069
  const _hoisted_13 = {
6068
- key: 2,
6070
+ key: 3,
6069
6071
  class: "fv-msg-pending"
6070
6072
  };
6071
6073
  const _hoisted_14 = {
@@ -6097,7 +6099,8 @@ ${fileParts.join("\n")}` });
6097
6099
  assistantAvatar: { type: String, required: false, default: "" },
6098
6100
  userName: { type: String, required: false, default: "" },
6099
6101
  assistantName: { type: String, required: false, default: "" },
6100
- autoCollapse: { type: Boolean, required: false, default: true }
6102
+ autoCollapse: { type: Boolean, required: false, default: true },
6103
+ typing: { type: [Boolean, Object], required: false, default: false }
6101
6104
  },
6102
6105
  emits: ["copy", "regenerate", "branch-change"],
6103
6106
  setup(__props, { expose: __expose, emit: __emit }) {
@@ -6112,6 +6115,9 @@ ${fileParts.join("\n")}` });
6112
6115
  });
6113
6116
  const hasReasoning = vue.computed(() => !!props.message.reasoning);
6114
6117
  const hasToolSteps = vue.computed(() => (props.message.toolSteps?.length ?? 0) > 0);
6118
+ const useTypewriter = vue.computed(
6119
+ () => !isUser.value && !!props.typing && (props.message.status === "pending" || props.message.status === "streaming")
6120
+ );
6115
6121
  const toolChainItems = vue.computed(() => {
6116
6122
  const steps = props.message.toolSteps ?? [];
6117
6123
  const running = props.message.status === "pending" || props.message.status === "streaming";
@@ -6247,7 +6253,7 @@ ${fileParts.join("\n")}` });
6247
6253
  class: "fv-msg-tools",
6248
6254
  items: toolChainItems.value
6249
6255
  }, null, 8, ["items"])) : vue.createCommentVNode("v-if", true),
6250
- vue.createCommentVNode(" \u601D\u8003\u94FE\u6298\u53E0(assistant \u542B reasoning \u65F6) "),
6256
+ vue.createCommentVNode(" \u601D\u8003\u94FE\u6298\u53E0(assistant \u542B reasoning \u65F6);\u6D41\u5F0F\u671F\u95F4\u7ECF\u6253\u5B57\u673A\u5E73\u6ED1\u6253\u51FA "),
6251
6257
  !isUser.value && hasReasoning.value ? (vue.openBlock(), vue.createBlock(_sfc_main$a, {
6252
6258
  key: 3,
6253
6259
  content: __props.message.reasoning,
@@ -6255,9 +6261,15 @@ ${fileParts.join("\n")}` });
6255
6261
  "auto-collapse": __props.autoCollapse
6256
6262
  }, {
6257
6263
  content: vue.withCtx(() => [
6258
- vue.createVNode(_sfc_main$i, {
6264
+ useTypewriter.value ? (vue.openBlock(), vue.createBlock(_sfc_main$h, {
6265
+ key: 0,
6266
+ content: __props.message.reasoning ?? "",
6267
+ "is-markdown": "",
6268
+ typing: __props.typing
6269
+ }, null, 8, ["content", "typing"])) : (vue.openBlock(), vue.createBlock(_sfc_main$i, {
6270
+ key: 1,
6259
6271
  content: __props.message.reasoning ?? ""
6260
- }, null, 8, ["content"])
6272
+ }, null, 8, ["content"]))
6261
6273
  ]),
6262
6274
  _: 1
6263
6275
  /* STABLE */
@@ -6270,8 +6282,13 @@ ${fileParts.join("\n")}` });
6270
6282
  vue.toDisplayString(__props.message.error || "\u751F\u6210\u5931\u8D25"),
6271
6283
  1
6272
6284
  /* TEXT */
6273
- )) : __props.message.content ? (vue.openBlock(), vue.createBlock(_sfc_main$i, {
6285
+ )) : useTypewriter.value && __props.message.content ? (vue.openBlock(), vue.createBlock(_sfc_main$h, {
6274
6286
  key: 1,
6287
+ content: __props.message.content,
6288
+ "is-markdown": "",
6289
+ typing: __props.typing
6290
+ }, null, 8, ["content", "typing"])) : __props.message.content ? (vue.openBlock(), vue.createBlock(_sfc_main$i, {
6291
+ key: 2,
6275
6292
  content: __props.message.content
6276
6293
  }, null, 8, ["content"])) : __props.message.status === "pending" || __props.message.status === "streaming" ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_13, [
6277
6294
  (vue.openBlock(), vue.createElementBlock(
@@ -6466,7 +6483,7 @@ ${fileParts.join("\n")}` });
6466
6483
  description: { type: String, required: false, default: "" },
6467
6484
  suggestions: { type: Array, required: false, default: () => [] },
6468
6485
  suggestionsTitle: { type: String, required: false, default: "" },
6469
- scrollThreshold: { type: Number, required: false, default: 40 },
6486
+ scrollThreshold: { type: Number, required: false, default: 100 },
6470
6487
  showScrollButton: { type: Boolean, required: false, default: true },
6471
6488
  scrollButtonThreshold: { type: Number, required: false, default: 100 },
6472
6489
  showActions: { type: Boolean, required: false, default: true },
@@ -6478,7 +6495,8 @@ ${fileParts.join("\n")}` });
6478
6495
  userAvatar: { type: String, required: false, default: "" },
6479
6496
  assistantAvatar: { type: String, required: false, default: "" },
6480
6497
  userName: { type: String, required: false, default: "" },
6481
- assistantName: { type: String, required: false, default: "" }
6498
+ assistantName: { type: String, required: false, default: "" },
6499
+ typing: { type: [Boolean, Object], required: false, default: false }
6482
6500
  },
6483
6501
  emits: ["select", "copy", "regenerate", "branch-change"],
6484
6502
  setup(__props, { expose: __expose, emit: __emit }) {
@@ -6491,21 +6509,35 @@ ${fileParts.join("\n")}` });
6491
6509
  if (!el) return 0;
6492
6510
  return el.scrollHeight - el.scrollTop - el.clientHeight;
6493
6511
  }
6512
+ const isPinnedToBottom = vue.ref(true);
6494
6513
  function scrollToBottom(smooth = true) {
6495
6514
  const el = scrollRef.value;
6496
6515
  if (!el) return;
6516
+ isPinnedToBottom.value = true;
6497
6517
  try {
6498
6518
  el.scrollTo({ top: el.scrollHeight, behavior: smooth ? "smooth" : "auto" });
6499
6519
  } catch {
6500
6520
  el.scrollTop = el.scrollHeight;
6501
6521
  }
6502
6522
  }
6503
- function scrollToBottomIfNear() {
6504
- if (distanceFromBottom() <= props.scrollThreshold) vue.nextTick(() => scrollToBottom());
6523
+ function scrollToBottomNow() {
6524
+ const el = scrollRef.value;
6525
+ if (!el) return;
6526
+ isPinnedToBottom.value = true;
6527
+ try {
6528
+ el.scrollTo({ top: el.scrollHeight, behavior: "auto" });
6529
+ } catch {
6530
+ el.scrollTop = el.scrollHeight;
6531
+ }
6532
+ }
6533
+ function followIfPinned() {
6534
+ if (isPinnedToBottom.value) vue.nextTick(() => scrollToBottomNow());
6505
6535
  }
6506
6536
  const isAwayFromBottom = vue.ref(false);
6507
6537
  function onScroll() {
6508
- isAwayFromBottom.value = distanceFromBottom() > props.scrollButtonThreshold;
6538
+ const d = distanceFromBottom();
6539
+ isPinnedToBottom.value = d <= props.scrollThreshold;
6540
+ isAwayFromBottom.value = d > props.scrollButtonThreshold;
6509
6541
  }
6510
6542
  vue.watch(() => props.scrollButtonThreshold, onScroll);
6511
6543
  vue.onMounted(() => {
@@ -6513,7 +6545,7 @@ ${fileParts.join("\n")}` });
6513
6545
  onScroll();
6514
6546
  });
6515
6547
  vue.watch(() => props.messages.length, (nv, ov) => {
6516
- if (nv > (ov ?? 0)) scrollToBottomIfNear();
6548
+ if (nv > (ov ?? 0)) followIfPinned();
6517
6549
  });
6518
6550
  vue.watch(
6519
6551
  () => props.messages.filter((m) => m.role === "user").length,
@@ -6526,11 +6558,11 @@ ${fileParts.join("\n")}` });
6526
6558
  (sum, m) => sum + m.content.length + (m.reasoning?.length ?? 0) + (m.toolSteps?.reduce((s, t) => s + (t.status?.length ?? 0) + (t.title?.length ?? 0), 0) ?? 0) + (m.toolSteps?.length ?? 0),
6527
6559
  0
6528
6560
  ),
6529
- () => scrollToBottomIfNear()
6561
+ () => followIfPinned()
6530
6562
  );
6531
6563
  vue.watch(
6532
6564
  () => props.messages.reduce((sum, m) => sum + (m.status === "done" || m.status === "error" ? 1 : 0), 0),
6533
- () => scrollToBottomIfNear()
6565
+ () => followIfPinned()
6534
6566
  );
6535
6567
  function branchInfo(m) {
6536
6568
  if (!m) return void 0;
@@ -6617,6 +6649,7 @@ ${fileParts.join("\n")}` });
6617
6649
  "assistant-avatar": __props.assistantAvatar,
6618
6650
  "user-name": __props.userName,
6619
6651
  "assistant-name": __props.assistantName,
6652
+ typing: __props.typing,
6620
6653
  onCopy,
6621
6654
  onRegenerate,
6622
6655
  onBranchChange: onBranch
@@ -6666,7 +6699,7 @@ ${fileParts.join("\n")}` });
6666
6699
  ]),
6667
6700
  key: "5"
6668
6701
  } : void 0
6669
- ]), 1032, ["message", "branch", "show-actions", "show-branch-switch", "show-user-avatar", "show-user-name", "show-assistant-avatar", "show-assistant-name", "user-avatar", "assistant-avatar", "user-name", "assistant-name"]);
6702
+ ]), 1032, ["message", "branch", "show-actions", "show-branch-switch", "show-user-avatar", "show-user-name", "show-assistant-avatar", "show-assistant-name", "user-avatar", "assistant-avatar", "user-name", "assistant-name", "typing"]);
6670
6703
  }),
6671
6704
  128
6672
6705
  /* KEYED_FRAGMENT */
@@ -7004,6 +7037,7 @@ ${fileParts.join("\n")}` });
7004
7037
  assistantName: { type: String, required: false, default: "" },
7005
7038
  placeholder: { type: String, required: false, default: "" },
7006
7039
  toolStepLabel: { type: Function, required: false, default: void 0 },
7040
+ typing: { type: [Boolean, Object], required: false, default: false },
7007
7041
  senderShadow: { type: Boolean, required: false, default: true },
7008
7042
  suggestions: { type: Array, required: false, default: () => [] },
7009
7043
  features: { type: Object, required: false, default: () => ({}) },
@@ -7277,6 +7311,7 @@ ${fileParts.join("\n")}` });
7277
7311
  "assistant-avatar": __props.assistantAvatar,
7278
7312
  "user-name": __props.userName,
7279
7313
  "assistant-name": __props.assistantName,
7314
+ typing: __props.typing,
7280
7315
  onSelect: onSelectSuggestion,
7281
7316
  onCopy,
7282
7317
  onRegenerate,
@@ -7348,7 +7383,7 @@ ${fileParts.join("\n")}` });
7348
7383
  ]),
7349
7384
  key: "8"
7350
7385
  } : void 0
7351
- ]), 1032, ["messages", "nodes", "name", "suggestions", "show-scroll-button", "scroll-button-threshold", "show-user-avatar", "show-user-name", "show-assistant-avatar", "show-assistant-name", "user-avatar", "assistant-avatar", "user-name", "assistant-name"]),
7386
+ ]), 1032, ["messages", "nodes", "name", "suggestions", "show-scroll-button", "scroll-button-threshold", "show-user-avatar", "show-user-name", "show-assistant-avatar", "show-assistant-name", "user-avatar", "assistant-avatar", "user-name", "assistant-name", "typing"]),
7352
7387
  __props.features?.artifacts && artifacts.value.length ? (vue.openBlock(), vue.createBlock(_sfc_main$1, {
7353
7388
  key: 0,
7354
7389
  artifacts: artifacts.value,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fv-ui/ai-chat",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "基于 Vue3 + Element Plus 的 AI 对话组件库",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",