@fv-ui/ai-chat 0.1.0 → 0.2.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/lib/index.js CHANGED
@@ -23,6 +23,13 @@
23
23
 
24
24
  var ElementPlusIconsVue__namespace = /*#__PURE__*/_interopNamespaceDefault(ElementPlusIconsVue);
25
25
 
26
+ function unwrapPlugin(plugin) {
27
+ const candidate = plugin && typeof plugin === "object" && "default" in plugin ? plugin.default : plugin;
28
+ if (typeof candidate !== "function") {
29
+ throw new TypeError("[XMarkdown] \u65E0\u6548\u7684 markdown-it \u63D2\u4EF6:\u9884\u671F\u51FD\u6570,\u6536\u5230 " + typeof candidate);
30
+ }
31
+ return candidate;
32
+ }
26
33
  function createMarkdownInstance(options) {
27
34
  const md = new MarkdownIt({
28
35
  html: true,
@@ -30,9 +37,9 @@
30
37
  linkify: true,
31
38
  highlight: options?.highlight
32
39
  });
33
- md.use(mdKatex);
40
+ md.use(unwrapPlugin(mdKatex));
34
41
  if (options?.mdPlugins) {
35
- for (const p of options.mdPlugins) md.use(p);
42
+ for (const p of options.mdPlugins) md.use(unwrapPlugin(p));
36
43
  }
37
44
  return md;
38
45
  }
@@ -1535,8 +1542,8 @@
1535
1542
  }
1536
1543
  }
1537
1544
 
1538
- const _hoisted_1$h = ["innerHTML"];
1539
- var _sfc_main$h = /* @__PURE__ */ vue.defineComponent({
1545
+ const _hoisted_1$i = ["innerHTML"];
1546
+ var _sfc_main$i = /* @__PURE__ */ vue.defineComponent({
1540
1547
  ...{ name: "FvMarkdown" },
1541
1548
  __name: "index",
1542
1549
  props: {
@@ -1559,12 +1566,12 @@
1559
1566
  return vue.openBlock(), vue.createElementBlock("div", {
1560
1567
  class: "fv-xmarkdown markdown-body",
1561
1568
  innerHTML: html.value
1562
- }, null, 8, _hoisted_1$h);
1569
+ }, null, 8, _hoisted_1$i);
1563
1570
  };
1564
1571
  }
1565
1572
  });
1566
1573
 
1567
- const _hoisted_1$g = {
1574
+ const _hoisted_1$h = {
1568
1575
  key: 1,
1569
1576
  class: "fv-typewriter-text"
1570
1577
  };
@@ -1572,7 +1579,7 @@
1572
1579
  key: 0,
1573
1580
  class: "fv-typewriter-suffix"
1574
1581
  };
1575
- var _sfc_main$g = /* @__PURE__ */ vue.defineComponent({
1582
+ var _sfc_main$h = /* @__PURE__ */ vue.defineComponent({
1576
1583
  ...{ name: "FvTypewriter" },
1577
1584
  __name: "index",
1578
1585
  props: {
@@ -1662,11 +1669,11 @@
1662
1669
  },
1663
1670
  [
1664
1671
  vue.createCommentVNode(" markdown \u6A21\u5F0F:suffix \u4E0D\u8FFD\u52A0(element-plus-x \u884C\u4E3A),isTyping \u900F\u4F20\u7ED9 XMarkdown \u4F5C\u6837\u5F0F\u94A9\u5B50 "),
1665
- __props.isMarkdown ? (vue.openBlock(), vue.createBlock(_sfc_main$h, {
1672
+ __props.isMarkdown ? (vue.openBlock(), vue.createBlock(_sfc_main$i, {
1666
1673
  key: 0,
1667
1674
  content: renderedContent.value,
1668
1675
  "is-typing": isTyping.value
1669
- }, null, 8, ["content", "is-typing"])) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_1$g, [
1676
+ }, null, 8, ["content", "is-typing"])) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_1$h, [
1670
1677
  vue.createTextVNode(
1671
1678
  vue.toDisplayString(renderedContent.value),
1672
1679
  1
@@ -1701,7 +1708,7 @@
1701
1708
  }
1702
1709
  });
1703
1710
 
1704
- const _hoisted_1$f = {
1711
+ const _hoisted_1$g = {
1705
1712
  key: 0,
1706
1713
  class: "fv-bubble-avatar"
1707
1714
  };
@@ -1722,7 +1729,7 @@
1722
1729
  key: 1,
1723
1730
  class: "fv-bubble-footer"
1724
1731
  };
1725
- var _sfc_main$f = /* @__PURE__ */ vue.defineComponent({
1732
+ var _sfc_main$g = /* @__PURE__ */ vue.defineComponent({
1726
1733
  ...{ name: "FvBubble" },
1727
1734
  __name: "index",
1728
1735
  props: {
@@ -1789,7 +1796,7 @@
1789
1796
  },
1790
1797
  [
1791
1798
  vue.createCommentVNode(" \u5934\u50CF:placement \u540C\u7F18\u6392\u5217(end \u7ECF row-reverse \u7FFB\u8F6C\u5230\u53F3\u4FA7),#avatar \u63D2\u69FD\u4F18\u5148 "),
1792
- _ctx.$slots.avatar || __props.avatar || __props.avatarIcon ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$f, [
1799
+ _ctx.$slots.avatar || __props.avatar || __props.avatarIcon ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$g, [
1793
1800
  _ctx.$slots.avatar ? vue.renderSlot(_ctx.$slots, "avatar", {}, void 0, void 0, 0) : (vue.openBlock(), vue.createBlock(vue.unref(elementPlus.ElAvatar), {
1794
1801
  key: 1,
1795
1802
  size: __props.avatarSize || void 0,
@@ -1847,7 +1854,7 @@
1847
1854
  { key: 2 },
1848
1855
  [
1849
1856
  vue.createCommentVNode(" typing \u900F\u4F20 Typewriter;\u975E typing:isMarkdown \u2192 XMarkdown,\u5426\u5219\u7EAF\u6587\u672C "),
1850
- vue.createVNode(_sfc_main$g, {
1857
+ vue.createVNode(_sfc_main$h, {
1851
1858
  ref_key: "twRef",
1852
1859
  ref: twRef,
1853
1860
  content: __props.content,
@@ -1861,7 +1868,7 @@
1861
1868
  ],
1862
1869
  2112
1863
1870
  /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */
1864
- )) : __props.isMarkdown ? (vue.openBlock(), vue.createBlock(_sfc_main$h, {
1871
+ )) : __props.isMarkdown ? (vue.openBlock(), vue.createBlock(_sfc_main$i, {
1865
1872
  key: 3,
1866
1873
  content: __props.content
1867
1874
  }, null, 8, ["content"])) : (vue.openBlock(), vue.createElementBlock(
@@ -1887,8 +1894,8 @@
1887
1894
  }
1888
1895
  });
1889
1896
 
1890
- const _hoisted_1$e = { class: "fv-bubble-list" };
1891
- var _sfc_main$e = /* @__PURE__ */ vue.defineComponent({
1897
+ const _hoisted_1$f = { class: "fv-bubble-list" };
1898
+ var _sfc_main$f = /* @__PURE__ */ vue.defineComponent({
1892
1899
  ...{ name: "FvBubbleList" },
1893
1900
  __name: "index",
1894
1901
  props: {
@@ -1988,7 +1995,7 @@
1988
1995
  }));
1989
1996
  __expose({ scrollToTop, scrollToBottom, scrollToBubble });
1990
1997
  return (_ctx, _cache) => {
1991
- return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$e, [
1998
+ return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$f, [
1992
1999
  vue.createElementVNode(
1993
2000
  "div",
1994
2001
  {
@@ -2003,7 +2010,7 @@
2003
2010
  vue.Fragment,
2004
2011
  null,
2005
2012
  vue.renderList(__props.list, (item, index) => {
2006
- return vue.openBlock(), vue.createBlock(_sfc_main$f, vue.mergeProps({
2013
+ return vue.openBlock(), vue.createBlock(_sfc_main$g, vue.mergeProps({
2007
2014
  key: item.key,
2008
2015
  ref_for: true,
2009
2016
  ref: (el) => setBubbleRef(el, index)
@@ -2111,6 +2118,139 @@
2111
2118
  }
2112
2119
  });
2113
2120
 
2121
+ const _hoisted_1$e = {
2122
+ class: "fv-stop-icon",
2123
+ xmlns: "http://www.w3.org/2000/svg",
2124
+ viewBox: "0 0 24 24",
2125
+ width: "1em",
2126
+ height: "1em",
2127
+ "aria-hidden": "true",
2128
+ focusable: "false"
2129
+ };
2130
+ var _sfc_main$e = /* @__PURE__ */ vue.defineComponent({
2131
+ ...{ name: "FvStopIcon" },
2132
+ __name: "StopIcon",
2133
+ setup(__props) {
2134
+ return (_ctx, _cache) => {
2135
+ return vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$e, [..._cache[0] || (_cache[0] = [
2136
+ vue.createElementVNode(
2137
+ "rect",
2138
+ {
2139
+ x: "5.5",
2140
+ y: "5.5",
2141
+ width: "13",
2142
+ height: "13",
2143
+ rx: "2.5",
2144
+ fill: "currentColor"
2145
+ },
2146
+ null,
2147
+ -1
2148
+ /* CACHED */
2149
+ )
2150
+ ])]);
2151
+ };
2152
+ }
2153
+ });
2154
+
2155
+ function getSpeechRecognitionCtor() {
2156
+ if (typeof window === "undefined") return void 0;
2157
+ const w = window;
2158
+ const ctor = w.SpeechRecognition ?? w.webkitSpeechRecognition;
2159
+ return typeof ctor === "function" ? ctor : void 0;
2160
+ }
2161
+ function useRecord() {
2162
+ const isRecording = vue.ref(false);
2163
+ const text = vue.ref("");
2164
+ const audioUrl = vue.ref(null);
2165
+ const error = vue.ref(null);
2166
+ let mediaStream = null;
2167
+ let mediaRecorder = null;
2168
+ let chunks = [];
2169
+ let recognition = null;
2170
+ let finalText = "";
2171
+ function revokeAudioUrl() {
2172
+ if (audioUrl.value) {
2173
+ URL.revokeObjectURL(audioUrl.value);
2174
+ audioUrl.value = null;
2175
+ }
2176
+ }
2177
+ async function start() {
2178
+ if (isRecording.value) return;
2179
+ error.value = null;
2180
+ text.value = "";
2181
+ finalText = "";
2182
+ chunks = [];
2183
+ revokeAudioUrl();
2184
+ if (typeof MediaRecorder === "undefined" || !navigator.mediaDevices?.getUserMedia) {
2185
+ error.value = new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u97F3\u9891\u5F55\u5236");
2186
+ return;
2187
+ }
2188
+ try {
2189
+ mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
2190
+ } catch (e) {
2191
+ error.value = e instanceof Error ? e : new Error(String(e));
2192
+ return;
2193
+ }
2194
+ mediaRecorder = new MediaRecorder(mediaStream);
2195
+ mediaRecorder.ondataavailable = (e) => {
2196
+ if (e.data && e.data.size > 0) chunks.push(e.data);
2197
+ };
2198
+ mediaRecorder.onstop = () => {
2199
+ if (chunks.length > 0) {
2200
+ const blob = new Blob(chunks, { type: mediaRecorder?.mimeType || "audio/webm" });
2201
+ audioUrl.value = URL.createObjectURL(blob);
2202
+ }
2203
+ mediaStream?.getTracks().forEach((t) => t.stop());
2204
+ mediaStream = null;
2205
+ };
2206
+ const SRCtor = getSpeechRecognitionCtor();
2207
+ if (SRCtor) {
2208
+ try {
2209
+ recognition = new SRCtor();
2210
+ recognition.continuous = true;
2211
+ recognition.interimResults = true;
2212
+ recognition.onresult = (event) => {
2213
+ let interim = "";
2214
+ for (let i = event.resultIndex; i < event.results.length; i++) {
2215
+ const r = event.results[i];
2216
+ if (r.isFinal) finalText += r[0].transcript;
2217
+ else interim += r[0].transcript;
2218
+ }
2219
+ text.value = finalText + interim;
2220
+ };
2221
+ recognition.onerror = (ev) => {
2222
+ error.value = new Error(`\u8BED\u97F3\u8BC6\u522B\u9519\u8BEF: ${ev?.error ?? "unknown"}`);
2223
+ };
2224
+ recognition.start();
2225
+ } catch {
2226
+ recognition = null;
2227
+ }
2228
+ }
2229
+ mediaRecorder.start();
2230
+ isRecording.value = true;
2231
+ }
2232
+ function stop() {
2233
+ if (!isRecording.value) return;
2234
+ isRecording.value = false;
2235
+ try {
2236
+ recognition?.stop();
2237
+ } catch {
2238
+ }
2239
+ recognition = null;
2240
+ try {
2241
+ if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop();
2242
+ } catch {
2243
+ }
2244
+ }
2245
+ if (vue.getCurrentInstance()) {
2246
+ vue.onBeforeUnmount(() => {
2247
+ stop();
2248
+ revokeAudioUrl();
2249
+ });
2250
+ }
2251
+ return { start, stop, isRecording, text, audioUrl, error };
2252
+ }
2253
+
2114
2254
  const _hoisted_1$d = {
2115
2255
  key: 0,
2116
2256
  class: "fv-sender-header"
@@ -2125,24 +2265,38 @@
2125
2265
  key: 0,
2126
2266
  class: "fv-sender-prefix"
2127
2267
  };
2128
- const _hoisted_6$5 = ["value", "placeholder", "disabled", "readonly"];
2129
- const _hoisted_7$4 = {
2130
- key: 1,
2268
+ const _hoisted_6$5 = {
2269
+ class: "fv-sender-recording-wave",
2270
+ "aria-hidden": "true"
2271
+ };
2272
+ const _hoisted_7$4 = { class: "fv-sender-recording-duration" };
2273
+ const _hoisted_8$3 = ["value", "placeholder", "disabled", "readonly"];
2274
+ const _hoisted_9$2 = {
2275
+ key: 3,
2131
2276
  class: "fv-sender-actions"
2132
2277
  };
2133
- const _hoisted_8$3 = {
2134
- key: 2,
2278
+ const _hoisted_10$2 = {
2279
+ key: 4,
2135
2280
  class: "fv-sender-actions"
2136
2281
  };
2137
- const _hoisted_9$2 = {
2282
+ const _hoisted_11$2 = {
2138
2283
  key: 0,
2139
2284
  class: "fv-sender-action-left"
2140
2285
  };
2141
- const _hoisted_10$2 = {
2286
+ const _hoisted_12$2 = {
2287
+ class: "fv-sender-stop-ring",
2288
+ "aria-hidden": "true"
2289
+ };
2290
+ const _hoisted_13$2 = ["rx"];
2291
+ const _hoisted_14$2 = {
2142
2292
  key: 1,
2143
2293
  class: "fv-sender-footer"
2144
2294
  };
2145
2295
  const ROW_HEIGHT = 22;
2296
+ const WAVE_BARS = 48;
2297
+ const RING_HEAD = 10;
2298
+ const RING_TAIL = 40;
2299
+ const RING_LAYERS = 6;
2146
2300
  var _sfc_main$d = /* @__PURE__ */ vue.defineComponent({
2147
2301
  ...{ name: "FvSender" },
2148
2302
  __name: "index",
@@ -2156,6 +2310,8 @@
2156
2310
  disabled: { type: Boolean, required: false, default: false },
2157
2311
  readOnly: { type: Boolean, required: false, default: false },
2158
2312
  clearable: { type: Boolean, required: false, default: false },
2313
+ round: { type: Boolean, required: false, default: true },
2314
+ shadow: { type: Boolean, required: false, default: false },
2159
2315
  inputWidth: { type: String, required: false, default: "" },
2160
2316
  variant: { type: String, required: false, default: "default" },
2161
2317
  submitBtnDisabled: { type: Boolean, required: false, default: false },
@@ -2328,11 +2484,136 @@
2328
2484
  for (const f of Array.from(files)) emit("paste-file", f);
2329
2485
  }
2330
2486
  }
2331
- function onSpeechClick() {
2332
- console.warn("useRecord \u672A\u63A5\u5165,Task 15 \u63A5\u7EBF");
2487
+ const { start: startRecord, stop: stopRecord, isRecording, text: recordText, error: recordError } = useRecord();
2488
+ async function exitRecordingFocusInput() {
2489
+ await vue.nextTick();
2490
+ focus("end");
2491
+ }
2492
+ function insertTextAtCursor(insert) {
2493
+ if (!insert) return;
2494
+ const el = textareaRef.value;
2495
+ const pos = el?.selectionStart ?? value.value.length;
2496
+ const next = value.value.slice(0, pos) + insert + value.value.slice(el?.selectionEnd ?? pos);
2497
+ setValue(next);
2498
+ vue.nextTick(() => {
2499
+ const t = textareaRef.value;
2500
+ if (t) t.setSelectionRange(pos + insert.length, pos + insert.length);
2501
+ });
2502
+ }
2503
+ async function onSpeechClick() {
2504
+ if (isRecording.value) return;
2505
+ await startRecord();
2506
+ if (recordError.value) {
2507
+ elementPlus.ElMessage.error(recordError.value.message || "\u65E0\u6CD5\u8BBF\u95EE\u9EA6\u514B\u98CE");
2508
+ return;
2509
+ }
2510
+ emit("recording-change", true);
2511
+ }
2512
+ async function onRecordConfirm() {
2513
+ stopRecord();
2514
+ emit("recording-change", false);
2515
+ insertTextAtCursor(recordText.value);
2516
+ await exitRecordingFocusInput();
2517
+ }
2518
+ async function onRecordCancel() {
2519
+ stopRecord();
2520
+ emit("recording-change", false);
2521
+ await exitRecordingFocusInput();
2333
2522
  }
2523
+ function onRecordingKeydown(e) {
2524
+ if (e.key === "Escape") {
2525
+ e.preventDefault();
2526
+ onRecordCancel();
2527
+ }
2528
+ }
2529
+ const waveLevels = vue.ref(Array(WAVE_BARS).fill(0.04));
2530
+ let audioCtx = null;
2531
+ let analyserSource = null;
2532
+ let waveRafId = 0;
2533
+ function normalizeRms(rms) {
2534
+ const scaled = Math.pow(rms * 10, 1.5);
2535
+ return Math.min(1, Math.max(0.04, scaled));
2536
+ }
2537
+ async function startWaveVisualizer() {
2538
+ if (typeof AudioContext === "undefined" || !navigator.mediaDevices?.getUserMedia) return;
2539
+ try {
2540
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
2541
+ audioCtx = new AudioContext();
2542
+ analyserSource = audioCtx.createMediaStreamSource(stream);
2543
+ const analyser = audioCtx.createAnalyser();
2544
+ analyser.minDecibels = -45;
2545
+ analyserSource.connect(analyser);
2546
+ const timeData = new Uint8Array(analyser.fftSize);
2547
+ const processFrame = () => {
2548
+ analyser.getByteTimeDomainData(timeData);
2549
+ let sumSquares = 0;
2550
+ for (let i = 0; i < timeData.length; i++) {
2551
+ const v = (timeData[i] - 128) / 128;
2552
+ sumSquares += v * v;
2553
+ }
2554
+ const level = normalizeRms(Math.sqrt(sumSquares / timeData.length));
2555
+ waveLevels.value = [...waveLevels.value.slice(1), level];
2556
+ waveRafId = requestAnimationFrame(processFrame);
2557
+ };
2558
+ waveRafId = requestAnimationFrame(processFrame);
2559
+ stream.getTracks().forEach((t) => t.addEventListener("ended", () => stopWaveVisualizer()));
2560
+ analyserSourceStream = stream;
2561
+ } catch {
2562
+ }
2563
+ }
2564
+ let analyserSourceStream = null;
2565
+ function stopWaveVisualizer() {
2566
+ cancelAnimationFrame(waveRafId);
2567
+ waveRafId = 0;
2568
+ analyserSource?.disconnect();
2569
+ analyserSource = null;
2570
+ analyserSourceStream?.getTracks().forEach((t) => t.stop());
2571
+ analyserSourceStream = null;
2572
+ audioCtx?.close().catch(() => {
2573
+ });
2574
+ audioCtx = null;
2575
+ waveLevels.value = Array(WAVE_BARS).fill(0.04);
2576
+ }
2577
+ vue.watch(isRecording, (rec) => {
2578
+ if (rec) startWaveVisualizer();
2579
+ else stopWaveVisualizer();
2580
+ });
2581
+ const recordSeconds = vue.ref(0);
2582
+ let recordTimer = null;
2583
+ vue.watch(isRecording, (rec) => {
2584
+ if (rec) {
2585
+ recordSeconds.value = 0;
2586
+ recordTimer = setInterval(() => {
2587
+ recordSeconds.value++;
2588
+ }, 1e3);
2589
+ } else if (recordTimer) {
2590
+ clearInterval(recordTimer);
2591
+ recordTimer = null;
2592
+ }
2593
+ });
2594
+ const recordDuration = vue.computed(() => {
2595
+ const m = Math.floor(recordSeconds.value / 60);
2596
+ const s = recordSeconds.value % 60;
2597
+ return `${m}:${String(s).padStart(2, "0")}`;
2598
+ });
2599
+ vue.onBeforeUnmount(() => {
2600
+ stopWaveVisualizer();
2601
+ if (recordTimer) clearInterval(recordTimer);
2602
+ });
2334
2603
  const inputDisabled = vue.computed(() => props.disabled || props.loading);
2335
2604
  const inputReadonly = vue.computed(() => props.readOnly);
2605
+ const ringLayers = vue.computed(
2606
+ () => Array.from({ length: RING_LAYERS }, (_, i) => {
2607
+ const len = RING_HEAD + RING_TAIL * i / (RING_LAYERS - 1);
2608
+ const alpha = 0.9 * Math.pow(0.72, i);
2609
+ return {
2610
+ style: {
2611
+ "--ring-len": len.toFixed(2),
2612
+ "--ring-alpha": alpha.toFixed(3)
2613
+ }
2614
+ };
2615
+ })
2616
+ );
2336
2617
  const slots = vue.useSlots();
2337
2618
  const hasActionListSlot = vue.computed(() => !!slots["action-list"]);
2338
2619
  __expose({ submit, cancel, clear, focus, blur, openHeader, closeHeader });
@@ -2340,7 +2621,7 @@
2340
2621
  return vue.openBlock(), vue.createElementBlock(
2341
2622
  "div",
2342
2623
  {
2343
- class: vue.normalizeClass(["fv-sender", [`variant-${__props.variant}`, { "is-disabled": __props.disabled, "is-loading": __props.loading, "is-focused": isFocused.value }]])
2624
+ class: vue.normalizeClass(["fv-sender", [`variant-${__props.variant}`, { "is-disabled": __props.disabled, "is-loading": __props.loading, "is-focused": isFocused.value, "has-shadow": __props.shadow }]])
2344
2625
  },
2345
2626
  [
2346
2627
  vue.createCommentVNode(" \u5934\u90E8:#header \u63D2\u69FD,openHeader/closeHeader \u63A7\u5236 "),
@@ -2358,9 +2639,89 @@
2358
2639
  _ctx.$slots.prefix ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_5$7, [
2359
2640
  vue.renderSlot(_ctx.$slots, "prefix")
2360
2641
  ])) : vue.createCommentVNode("v-if", true),
2361
- vue.createElementVNode(
2642
+ vue.createCommentVNode(" \u8BED\u97F3\u5F55\u97F3\u4E2D:\u8F93\u5165\u533A\u539F\u4F4D\u66FF\u6362\u4E3A\u5F55\u97F3\u6761(\u53D6\u6D88/\u6CE2\u5F62+\u65F6\u957F/\u786E\u8BA4);Esc \u53D6\u6D88 "),
2643
+ vue.unref(isRecording) ? (vue.openBlock(), vue.createElementBlock(
2362
2644
  "div",
2363
2645
  {
2646
+ key: 1,
2647
+ class: "fv-sender-recording",
2648
+ tabindex: "-1",
2649
+ onKeydown: onRecordingKeydown
2650
+ },
2651
+ [
2652
+ vue.createVNode(vue.unref(elementPlus.ElButton), {
2653
+ class: "fv-sender-recording-cancel",
2654
+ text: "",
2655
+ circle: "",
2656
+ "aria-label": "\u53D6\u6D88\u5F55\u97F3",
2657
+ onClick: onRecordCancel
2658
+ }, {
2659
+ default: vue.withCtx(() => [
2660
+ vue.createVNode(vue.unref(elementPlus.ElIcon), null, {
2661
+ default: vue.withCtx(() => [
2662
+ vue.createVNode(vue.unref(ElementPlusIconsVue.Close))
2663
+ ]),
2664
+ _: 1
2665
+ /* STABLE */
2666
+ })
2667
+ ]),
2668
+ _: 1
2669
+ /* STABLE */
2670
+ }),
2671
+ vue.createElementVNode("div", _hoisted_6$5, [
2672
+ (vue.openBlock(true), vue.createElementBlock(
2673
+ vue.Fragment,
2674
+ null,
2675
+ vue.renderList(waveLevels.value, (lv, i) => {
2676
+ return vue.openBlock(), vue.createElementBlock(
2677
+ "span",
2678
+ {
2679
+ key: i,
2680
+ class: "fv-sender-recording-bar",
2681
+ style: vue.normalizeStyle({ height: `${Math.round(lv * 100)}%` })
2682
+ },
2683
+ null,
2684
+ 4
2685
+ /* STYLE */
2686
+ );
2687
+ }),
2688
+ 128
2689
+ /* KEYED_FRAGMENT */
2690
+ ))
2691
+ ]),
2692
+ vue.createElementVNode(
2693
+ "span",
2694
+ _hoisted_7$4,
2695
+ vue.toDisplayString(recordDuration.value),
2696
+ 1
2697
+ /* TEXT */
2698
+ ),
2699
+ vue.createVNode(vue.unref(elementPlus.ElButton), {
2700
+ class: "fv-sender-recording-confirm",
2701
+ type: "primary",
2702
+ circle: "",
2703
+ "aria-label": "\u786E\u8BA4\u5F55\u97F3",
2704
+ onClick: onRecordConfirm
2705
+ }, {
2706
+ default: vue.withCtx(() => [
2707
+ vue.createVNode(vue.unref(elementPlus.ElIcon), null, {
2708
+ default: vue.withCtx(() => [
2709
+ vue.createVNode(vue.unref(ElementPlusIconsVue.Check))
2710
+ ]),
2711
+ _: 1
2712
+ /* STABLE */
2713
+ })
2714
+ ]),
2715
+ _: 1
2716
+ /* STABLE */
2717
+ })
2718
+ ],
2719
+ 32
2720
+ /* NEED_HYDRATION */
2721
+ )) : (vue.openBlock(), vue.createElementBlock(
2722
+ "div",
2723
+ {
2724
+ key: 2,
2364
2725
  class: "fv-sender-input",
2365
2726
  style: vue.normalizeStyle(__props.inputWidth && __props.variant === "default" ? { width: __props.inputWidth } : void 0)
2366
2727
  },
@@ -2380,7 +2741,7 @@
2380
2741
  onPaste,
2381
2742
  onFocus: _cache[0] || (_cache[0] = ($event) => isFocused.value = true),
2382
2743
  onBlur: onTextareaBlur
2383
- }, null, 40, _hoisted_6$5),
2744
+ }, null, 40, _hoisted_8$3),
2384
2745
  vue.createCommentVNode(" \u6307\u4EE4\u5F39\u6846:\u5185\u7F6E el-popover,\u5B9A\u4F4D\u6309 triggerPopover* \u7CFB\u5217 "),
2385
2746
  vue.createVNode(vue.unref(elementPlus.ElPopover), {
2386
2747
  visible: triggerVisible.value && __props.triggerStrings.length > 0,
@@ -2411,12 +2772,12 @@
2411
2772
  ],
2412
2773
  4
2413
2774
  /* STYLE */
2414
- ),
2775
+ )),
2415
2776
  vue.createCommentVNode(" \u64CD\u4F5C\u533A:#action-list \u5B58\u5728\u65F6\u5168\u81EA\u5B9A\u4E49,\u9690\u85CF\u5185\u7F6E\u6309\u94AE;#action-left \u7F6E\u4E8E\u64CD\u4F5C\u680F\u5DE6\u4FA7(\u4E0E\u63D0\u4EA4\u6309\u94AE\u540C\u884C) "),
2416
- hasActionListSlot.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_7$4, [
2777
+ hasActionListSlot.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9$2, [
2417
2778
  vue.renderSlot(_ctx.$slots, "action-list")
2418
- ])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_8$3, [
2419
- _ctx.$slots["action-left"] ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_9$2, [
2779
+ ])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_10$2, [
2780
+ _ctx.$slots["action-left"] ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_11$2, [
2420
2781
  vue.renderSlot(_ctx.$slots, "action-left")
2421
2782
  ])) : vue.createCommentVNode("v-if", true),
2422
2783
  __props.clearable && value.value ? (vue.openBlock(), vue.createBlock(vue.unref(elementPlus.ElButton), {
@@ -2439,7 +2800,7 @@
2439
2800
  _: 1
2440
2801
  /* STABLE */
2441
2802
  }, 8, ["disabled"])) : vue.createCommentVNode("v-if", true),
2442
- __props.allowSpeech ? (vue.openBlock(), vue.createBlock(vue.unref(elementPlus.ElButton), {
2803
+ __props.allowSpeech && !vue.unref(isRecording) ? (vue.openBlock(), vue.createBlock(vue.unref(elementPlus.ElButton), {
2443
2804
  key: 2,
2444
2805
  class: "fv-sender-speech",
2445
2806
  text: "",
@@ -2459,29 +2820,49 @@
2459
2820
  _: 1
2460
2821
  /* STABLE */
2461
2822
  }, 8, ["disabled"])) : vue.createCommentVNode("v-if", true),
2462
- vue.createCommentVNode(" loading \u2192 \u505C\u6B62\u6309\u94AE;\u5426\u5219\u53D1\u9001\u6309\u94AE "),
2823
+ vue.createCommentVNode(" loading \u2192 \u505C\u6B62\u6309\u94AE:SVG \u5149\u5E26\u6CBF\u8FB9\u6846\u5300\u901F\u6D41\u8F6C,\u65B9/\u5706\u6309\u94AE\u5747\u9002\u7528 "),
2463
2824
  __props.loading ? (vue.openBlock(), vue.createBlock(vue.unref(elementPlus.ElButton), {
2464
2825
  key: 3,
2465
2826
  class: "fv-sender-stop",
2466
2827
  type: "primary",
2828
+ circle: __props.round,
2467
2829
  "aria-label": "\u505C\u6B62",
2468
2830
  onClick: cancel
2469
2831
  }, {
2470
2832
  default: vue.withCtx(() => [
2471
2833
  vue.createVNode(vue.unref(elementPlus.ElIcon), null, {
2472
2834
  default: vue.withCtx(() => [
2473
- vue.createVNode(vue.unref(ElementPlusIconsVue.VideoPause))
2835
+ vue.createVNode(_sfc_main$e)
2474
2836
  ]),
2475
2837
  _: 1
2476
2838
  /* STABLE */
2477
- })
2839
+ }),
2840
+ vue.createCommentVNode('\r\n \u6CBF\u8FB9\u6846\u6D41\u8F6C\u7684\u6E10\u53D8\u5149\u5E26(\u8682\u8681\u7EBF\u539F\u7406,pathLength=100 \u5F52\u4E00\u5468\u957F,dashoffset \u5300\u901F\u5E73\u79FB)\u3002\r\n \u6E10\u53D8\u96BE\u70B9:linearGradient \u6CBF\u76F4\u7EBF\u8DEF\u5F84\u5B9A\u4E49,\u65E0\u6CD5\u6CBF\u95ED\u5408\u6298\u7EBF\u5468\u957F\u6E10\u53D8 \u2014\u2014\r\n \u6545\u7528 6 \u5C42\u957F\u77ED/\u900F\u660E\u5EA6\u53CC\u9012\u51CF\u7684\u540C\u76F8\u4F4D dash \u53E0\u52A0,\u79BB\u6563\u9636\u68AF\u903C\u8FD1\u8FDE\u7EED\u6E10\u53D8:\r\n \u6BCF\u5C42\u957F\u5EA6\u4F9D\u6B21\u9012\u589E(\u5E26\u5934\u6700\u77ED\u6700\u5B9E,\u5E26\u5C3E\u6700\u957F\u6700\u865A),\u900F\u660E\u5EA6\u4F9D\u6B21\u9012\u51CF,\r\n \u53E0\u52A0\u540E\u7684\u900F\u660E\u5EA6\u5256\u9762\u4E3A\u591A\u7EA7\u53F0\u9636 \u2192 \u8FD0\u52A8\u4E2D\u8BFB\u4F5C"\u5934\u4EAE\u5C3E\u9690"\u7684\u8FDE\u7EED\u62D6\u5F71\u3002\r\n \u5C42\u6570\u7531 RING_LAYERS \u5E38\u91CF\u9A71\u52A8,\u900F\u660E\u5EA6\u63D2\u503C\u5728 CSS \u91CC\u6309 --ring-alpha \u53D8\u91CF\u7B97\u3002\r\n '),
2841
+ (vue.openBlock(), vue.createElementBlock("svg", _hoisted_12$2, [
2842
+ (vue.openBlock(true), vue.createElementBlock(
2843
+ vue.Fragment,
2844
+ null,
2845
+ vue.renderList(ringLayers.value, (layer, i) => {
2846
+ return vue.openBlock(), vue.createElementBlock("rect", {
2847
+ key: i,
2848
+ class: "fv-sender-stop-ring-rect",
2849
+ style: vue.normalizeStyle(layer.style),
2850
+ rx: __props.round ? "50%" : 2.5,
2851
+ pathLength: "100"
2852
+ }, null, 12, _hoisted_13$2);
2853
+ }),
2854
+ 128
2855
+ /* KEYED_FRAGMENT */
2856
+ ))
2857
+ ]))
2478
2858
  ]),
2479
2859
  _: 1
2480
2860
  /* STABLE */
2481
- })) : (vue.openBlock(), vue.createBlock(vue.unref(elementPlus.ElButton), {
2861
+ }, 8, ["circle"])) : (vue.openBlock(), vue.createBlock(vue.unref(elementPlus.ElButton), {
2482
2862
  key: 4,
2483
2863
  class: "fv-sender-submit",
2484
2864
  type: "primary",
2865
+ circle: __props.round,
2485
2866
  disabled: !submitable(),
2486
2867
  "aria-label": "\u53D1\u9001",
2487
2868
  onClick: doSubmit
@@ -2497,13 +2878,13 @@
2497
2878
  ]),
2498
2879
  _: 1
2499
2880
  /* STABLE */
2500
- }, 8, ["disabled"]))
2881
+ }, 8, ["circle", "disabled"]))
2501
2882
  ]))
2502
2883
  ]),
2503
2884
  vue.createCommentVNode(" /fv-sender-row ")
2504
2885
  ]),
2505
2886
  vue.createCommentVNode(" \u5E95\u90E8 "),
2506
- _ctx.$slots.footer ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_10$2, [
2887
+ _ctx.$slots.footer ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_14$2, [
2507
2888
  vue.renderSlot(_ctx.$slots, "footer")
2508
2889
  ])) : vue.createCommentVNode("v-if", true)
2509
2890
  ],
@@ -5808,7 +6189,7 @@ ${fileParts.join("\n")}` });
5808
6189
  "auto-collapse": __props.autoCollapse
5809
6190
  }, {
5810
6191
  content: vue.withCtx(() => [
5811
- vue.createVNode(_sfc_main$h, {
6192
+ vue.createVNode(_sfc_main$i, {
5812
6193
  content: __props.message.reasoning ?? ""
5813
6194
  }, null, 8, ["content"])
5814
6195
  ]),
@@ -5823,7 +6204,7 @@ ${fileParts.join("\n")}` });
5823
6204
  vue.toDisplayString(__props.message.error || "\u751F\u6210\u5931\u8D25"),
5824
6205
  1
5825
6206
  /* TEXT */
5826
- )) : __props.message.content ? (vue.openBlock(), vue.createBlock(_sfc_main$h, {
6207
+ )) : __props.message.content ? (vue.openBlock(), vue.createBlock(_sfc_main$i, {
5827
6208
  key: 1,
5828
6209
  content: __props.message.content
5829
6210
  }, null, 8, ["content"])) : __props.message.status === "pending" || __props.message.status === "streaming" ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_12, [
@@ -6394,7 +6775,10 @@ ${fileParts.join("\n")}` });
6394
6775
  }
6395
6776
  });
6396
6777
 
6397
- const _hoisted_1 = { class: "fv-ai-chat-header" };
6778
+ const _hoisted_1 = {
6779
+ key: 0,
6780
+ class: "fv-ai-chat-header"
6781
+ };
6398
6782
  const _hoisted_2 = { class: "fv-ai-chat-title" };
6399
6783
  const _hoisted_3 = { class: "fv-ai-chat-body" };
6400
6784
  const _hoisted_4 = { class: "fv-ai-chat-footer" };
@@ -6409,7 +6793,10 @@ ${fileParts.join("\n")}` });
6409
6793
  adapter: { type: Object, required: false, default: void 0 },
6410
6794
  messages: { type: Array, required: false, default: void 0 },
6411
6795
  title: { type: String, required: false, default: "" },
6796
+ showHeader: { type: Boolean, required: false, default: false },
6797
+ centerWelcome: { type: Boolean, required: false, default: true },
6412
6798
  placeholder: { type: String, required: false, default: "" },
6799
+ senderShadow: { type: Boolean, required: false, default: true },
6413
6800
  suggestions: { type: Array, required: false, default: () => [] },
6414
6801
  features: { type: Object, required: false, default: () => ({}) },
6415
6802
  allowAttachments: { type: Boolean, required: false, default: false },
@@ -6457,6 +6844,7 @@ ${fileParts.join("\n")}` });
6457
6844
  const { send, stop: stopChat, regenerate: regenerateChat, msgs, isLoading } = chat;
6458
6845
  const nodeTable = chat.messages;
6459
6846
  const chain = chat.chain;
6847
+ const isCenterWelcome = vue.computed(() => props.centerWelcome && chain.value.length === 0);
6460
6848
  let flushTimer = null;
6461
6849
  vue.watch(chain, (c) => {
6462
6850
  if (flushTimer) clearTimeout(flushTimer);
@@ -6622,11 +7010,11 @@ ${fileParts.join("\n")}` });
6622
7010
  return vue.openBlock(), vue.createElementBlock(
6623
7011
  "div",
6624
7012
  {
6625
- class: vue.normalizeClass(["fv-ai-chat", { "is-dark": isDark.value }])
7013
+ class: vue.normalizeClass(["fv-ai-chat", { "is-dark": isDark.value, "is-center-welcome": isCenterWelcome.value }])
6626
7014
  },
6627
7015
  [
6628
7016
  vue.createCommentVNode(" \u9876\u680F:\u6807\u9898 + \u6A21\u578B\u9009\u62E9\u5668(models \u6709\u503C\u65F6 multiple) "),
6629
- vue.createElementVNode("div", _hoisted_1, [
7017
+ __props.showHeader ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
6630
7018
  vue.createElementVNode(
6631
7019
  "div",
6632
7020
  _hoisted_2,
@@ -6662,7 +7050,7 @@ ${fileParts.join("\n")}` });
6662
7050
  _: 1
6663
7051
  /* STABLE */
6664
7052
  }, 8, ["modelValue"])) : vue.createCommentVNode("v-if", true)
6665
- ]),
7053
+ ])) : vue.createCommentVNode("v-if", true),
6666
7054
  vue.createCommentVNode(" \u6D88\u606F\u533A:\u7A7A\u65F6 Welcome + Prompts(AiMessages \u5185\u7F6E);\u5F00\u542F artifacts \u65F6\u53F3\u4FA7\u62FC\u9884\u89C8\u9762\u677F "),
6667
7055
  vue.createElementVNode("div", _hoisted_3, [
6668
7056
  vue.createVNode(_sfc_main$2, {
@@ -6713,6 +7101,7 @@ ${fileParts.join("\n")}` });
6713
7101
  vue.createVNode(_sfc_main$d, {
6714
7102
  placeholder: __props.placeholder,
6715
7103
  loading: vue.unref(isLoading),
7104
+ shadow: __props.senderShadow,
6716
7105
  variant: "updown",
6717
7106
  maxlength: 8e3,
6718
7107
  "allow-empty-submit": __props.allowAttachments && !!pendingFiles.value.length,
@@ -6757,7 +7146,7 @@ ${fileParts.join("\n")}` });
6757
7146
  ]),
6758
7147
  key: "2"
6759
7148
  } : void 0
6760
- ]), 1032, ["placeholder", "loading", "allow-empty-submit"])
7149
+ ]), 1032, ["placeholder", "loading", "shadow", "allow-empty-submit"])
6761
7150
  ])
6762
7151
  ],
6763
7152
  2
@@ -6888,105 +7277,6 @@ ${fileParts.join("\n")}` });
6888
7277
  });
6889
7278
  }
6890
7279
 
6891
- function getSpeechRecognitionCtor() {
6892
- if (typeof window === "undefined") return void 0;
6893
- const w = window;
6894
- const ctor = w.SpeechRecognition ?? w.webkitSpeechRecognition;
6895
- return typeof ctor === "function" ? ctor : void 0;
6896
- }
6897
- function useRecord() {
6898
- const isRecording = vue.ref(false);
6899
- const text = vue.ref("");
6900
- const audioUrl = vue.ref(null);
6901
- const error = vue.ref(null);
6902
- let mediaStream = null;
6903
- let mediaRecorder = null;
6904
- let chunks = [];
6905
- let recognition = null;
6906
- let finalText = "";
6907
- function revokeAudioUrl() {
6908
- if (audioUrl.value) {
6909
- URL.revokeObjectURL(audioUrl.value);
6910
- audioUrl.value = null;
6911
- }
6912
- }
6913
- async function start() {
6914
- if (isRecording.value) return;
6915
- error.value = null;
6916
- text.value = "";
6917
- finalText = "";
6918
- chunks = [];
6919
- revokeAudioUrl();
6920
- if (typeof MediaRecorder === "undefined" || !navigator.mediaDevices?.getUserMedia) {
6921
- error.value = new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u97F3\u9891\u5F55\u5236");
6922
- return;
6923
- }
6924
- try {
6925
- mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
6926
- } catch (e) {
6927
- error.value = e instanceof Error ? e : new Error(String(e));
6928
- return;
6929
- }
6930
- mediaRecorder = new MediaRecorder(mediaStream);
6931
- mediaRecorder.ondataavailable = (e) => {
6932
- if (e.data && e.data.size > 0) chunks.push(e.data);
6933
- };
6934
- mediaRecorder.onstop = () => {
6935
- if (chunks.length > 0) {
6936
- const blob = new Blob(chunks, { type: mediaRecorder?.mimeType || "audio/webm" });
6937
- audioUrl.value = URL.createObjectURL(blob);
6938
- }
6939
- mediaStream?.getTracks().forEach((t) => t.stop());
6940
- mediaStream = null;
6941
- };
6942
- const SRCtor = getSpeechRecognitionCtor();
6943
- if (SRCtor) {
6944
- try {
6945
- recognition = new SRCtor();
6946
- recognition.continuous = true;
6947
- recognition.interimResults = true;
6948
- recognition.onresult = (event) => {
6949
- let interim = "";
6950
- for (let i = event.resultIndex; i < event.results.length; i++) {
6951
- const r = event.results[i];
6952
- if (r.isFinal) finalText += r[0].transcript;
6953
- else interim += r[0].transcript;
6954
- }
6955
- text.value = finalText + interim;
6956
- };
6957
- recognition.onerror = (ev) => {
6958
- error.value = new Error(`\u8BED\u97F3\u8BC6\u522B\u9519\u8BEF: ${ev?.error ?? "unknown"}`);
6959
- };
6960
- recognition.start();
6961
- } catch {
6962
- recognition = null;
6963
- }
6964
- }
6965
- mediaRecorder.start();
6966
- isRecording.value = true;
6967
- }
6968
- function stop() {
6969
- if (!isRecording.value) return;
6970
- isRecording.value = false;
6971
- try {
6972
- recognition?.stop();
6973
- } catch {
6974
- }
6975
- recognition = null;
6976
- try {
6977
- if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop();
6978
- } catch {
6979
- }
6980
- }
6981
- if (vue.getCurrentInstance()) {
6982
- vue.onBeforeUnmount(() => {
6983
- stop();
6984
- revokeAudioUrl();
6985
- });
6986
- }
6987
- return { start, stop, isRecording, text, audioUrl, error };
6988
- }
6989
-
6990
7280
  class DetOmsError extends Error {
6991
7281
  constructor(message, code) {
6992
7282
  super(message);
@@ -7189,18 +7479,18 @@ ${fileParts.join("\n")}` });
7189
7479
  exports.FvAiMessage = _sfc_main$3;
7190
7480
  exports.FvAiMessages = _sfc_main$2;
7191
7481
  exports.FvAttachments = _sfc_main$5;
7192
- exports.FvBubble = _sfc_main$f;
7193
- exports.FvBubbleList = _sfc_main$e;
7482
+ exports.FvBubble = _sfc_main$g;
7483
+ exports.FvBubbleList = _sfc_main$f;
7194
7484
  exports.FvConversations = _sfc_main$4;
7195
7485
  exports.FvEditorSender = _sfc_main$b;
7196
7486
  exports.FvFilesCard = _sfc_main$6;
7197
- exports.FvMarkdown = _sfc_main$h;
7487
+ exports.FvMarkdown = _sfc_main$i;
7198
7488
  exports.FvMentionSender = _sfc_main$c;
7199
7489
  exports.FvPrompts = _sfc_main$7;
7200
7490
  exports.FvSender = _sfc_main$d;
7201
7491
  exports.FvThinking = _sfc_main$a;
7202
7492
  exports.FvThoughtChain = _sfc_main$9;
7203
- exports.FvTypewriter = _sfc_main$g;
7493
+ exports.FvTypewriter = _sfc_main$h;
7204
7494
  exports.FvWelcome = _sfc_main$8;
7205
7495
  exports.OpenAIAdapter = OpenAIAdapter;
7206
7496
  exports.XRequest = XRequest;