@seer-ai/seer-vue 0.1.0-beta.6 → 0.1.0-beta.7

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
@@ -20,6 +20,7 @@ createApp(App)
20
20
  .use(SeerChatPlugin, {
21
21
  sessionEndpoint: 'https://seer.example.com/v1/browser/session',
22
22
  endpoint: 'https://seer.example.com/v1/browser/chat/completions',
23
+ transcriptionEndpoint: 'https://seer.example.com/v1/browser/audio/transcriptions',
23
24
  model: 'seer',
24
25
  stream: false,
25
26
  })
@@ -36,6 +37,10 @@ import { SeerChat } from '@seer-ai/seer-vue'
36
37
  </template>
37
38
  ```
38
39
 
40
+ 支持 MediaRecorder 的 HTTPS/localhost 页面会显示话筒。用户点击开始、再次点击停止,最长录音
41
+ 60 秒;SenseVoice 转写结果只回填输入框,不会自动发送。标准 browser chat endpoint 会自动
42
+ 推导 transcription endpoint,也可显式配置;传 `voiceEnabled: false` 可关闭话筒。
43
+
39
44
  ## Use custom UI
40
45
 
41
46
  自定义 UI 时**不要**直接把 `message.content` 当纯文本渲染——回复里的下载/截图链接是裸
@@ -75,4 +80,7 @@ async function submit() {
75
80
 
76
81
  ## Security
77
82
 
78
- Do not put long-lived seer API keys in browser code or `VITE_*` variables. Browser code should call seer's browser session endpoint, receive a short-lived token, and use that token for `/v1/browser/chat/completions`.
83
+ Do not put long-lived seer API keys in browser code or `VITE_*` variables. Browser code should call
84
+ seer's browser session endpoint, receive a short-lived token, and use that token for
85
+ `/v1/browser/chat/completions` and `/v1/browser/audio/transcriptions`. Raw audio is uploaded only for
86
+ the current transcription and must not be persisted.
package/dist/client.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface SeerChatMessage {
7
7
  export interface SeerClientOptions {
8
8
  endpoint: string;
9
9
  sessionEndpoint?: string;
10
+ transcriptionEndpoint?: string;
10
11
  challengeTokenProvider?: () => Promise<string>;
11
12
  model?: string;
12
13
  fetcher?: typeof fetch;
@@ -23,6 +24,12 @@ export interface SeerChatCallbacks {
23
24
  signal?: AbortSignal;
24
25
  onDelta?: (delta: string) => void;
25
26
  }
27
+ export interface SeerTranscriptionCallbacks {
28
+ signal?: AbortSignal;
29
+ }
30
+ export interface SeerTranscriptionResult {
31
+ text: string;
32
+ }
26
33
  export interface SeerChatResult {
27
34
  content: string;
28
35
  conversationId?: string;
@@ -31,6 +38,8 @@ export interface SeerChatResult {
31
38
  finishReason?: string;
32
39
  }
33
40
  export interface SeerClient {
41
+ transcriptionEnabled: boolean;
34
42
  chat(request: SeerChatRequest, callbacks?: SeerChatCallbacks): Promise<SeerChatResult>;
43
+ transcribe(audio: Blob, filename: string, callbacks?: SeerTranscriptionCallbacks): Promise<SeerTranscriptionResult>;
35
44
  }
36
45
  export declare function createSeerClient(options: SeerClientOptions): SeerClient;
@@ -1,17 +1,21 @@
1
1
  import type { SeerClientOptions } from "../client";
2
2
  type __VLS_Props = {
3
3
  endpoint?: string;
4
+ sessionEndpoint?: string;
5
+ transcriptionEndpoint?: string;
4
6
  model?: string;
5
7
  stream?: boolean;
6
8
  fetcher?: SeerClientOptions["fetcher"];
7
9
  title?: string;
8
10
  placeholder?: string;
11
+ voiceEnabled?: boolean;
12
+ maxRecordingSeconds?: number;
9
13
  };
10
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
15
  stream: boolean;
12
16
  title: string;
13
17
  placeholder: string;
14
- model: string;
18
+ voiceEnabled: boolean;
15
19
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
20
  declare const _default: typeof __VLS_export;
17
21
  export default _default;
package/dist/index.css CHANGED
@@ -1 +1,2 @@
1
- .seer-chat[data-v-dc651545]{display:grid;gap:18px;max-width:720px;min-height:640px;padding:0;overflow:hidden;color:#18212f;background:radial-gradient(circle at 18% 0%,rgba(71,106,255,.14),transparent 32%),linear-gradient(180deg,#fff,#f7f9fd);border:1px solid rgba(127,143,170,.28);border-radius:28px;box-shadow:0 24px 70px #151f3429}.seer-chat__header[data-v-dc651545]{display:flex;gap:16px;align-items:flex-start;justify-content:space-between;padding:22px 22px 18px;color:#fff;background:linear-gradient(135deg,#141f34f5,#243f76f0),#172033}.seer-chat__header h2[data-v-dc651545]{margin:0;font-size:22px;letter-spacing:-.03em}.seer-chat__header p[data-v-dc651545],.seer-chat__empty[data-v-dc651545],.seer-chat__status[data-v-dc651545]{margin:4px 0 0;color:#5f6f89;font-size:14px}.seer-chat__header p[data-v-dc651545]{max-width:34ch;color:#ffffffb8}.seer-chat__messages[data-v-dc651545]{display:grid;gap:12px;align-content:start;min-height:330px;max-height:470px;padding:18px 18px 4px;overflow-y:auto}.seer-chat__empty[data-v-dc651545]{display:grid;place-items:center;min-height:260px;padding:0 28px;color:#66748a;text-align:center;border:1px dashed rgba(127,143,170,.38);border-radius:22px;background:#ffffff9e}.seer-chat__message[data-v-dc651545]{display:grid;gap:6px;max-width:min(86%,520px);padding:13px 15px;border:1px solid rgba(127,143,170,.16);border-radius:18px 18px 18px 6px;background:#ffffffe0;box-shadow:0 12px 28px #151f3414}.seer-chat__message--user[data-v-dc651545]{justify-self:end;color:#fff;border-color:transparent;border-radius:18px 18px 6px;background:linear-gradient(135deg,#285cff,#1d45c9);box-shadow:0 14px 30px #1f5eff3d}.seer-chat__role[data-v-dc651545]{font-size:12px;font-weight:700;opacity:.75}.seer-chat__body[data-v-dc651545]{display:flex;flex-direction:column}.seer-chat__line[data-v-dc651545]{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}.seer-chat__line[data-v-dc651545]:not(:last-child){margin-bottom:5px}.seer-chat__line[data-v-dc651545]:empty{height:7px}.seer-chat__strong[data-v-dc651545]{font-weight:700}.seer-chat__price[data-v-dc651545]{color:#e0331d;font-weight:600}.seer-chat__message--user .seer-chat__price[data-v-dc651545]{color:#ffe2dc}.seer-chat__link[data-v-dc651545]{color:#1f5eff;font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}.seer-chat__link[data-v-dc651545]:hover{color:#173fb3}.seer-chat__message--user .seer-chat__link[data-v-dc651545]{color:#fff}.seer-chat__image-button[data-v-dc651545]{display:block;padding:0;background:transparent;border:0;cursor:zoom-in}.seer-chat__image-button[data-v-dc651545]:not(:disabled):hover{transform:none}.seer-chat__image[data-v-dc651545]{display:block;max-width:100%;margin:6px 0;border-radius:12px;border:1px solid rgba(127,143,170,.22);transition:filter .16s ease}.seer-chat__image-button:hover .seer-chat__image[data-v-dc651545]{filter:brightness(.96)}.seer-chat__lightbox[data-v-dc651545]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;display:grid;place-items:center;padding:32px;background:#0a0f1ad1;cursor:zoom-out;animation:seer-chat__fade-in-dc651545 .14s ease}.seer-chat__lightbox-image[data-v-dc651545]{max-width:min(92vw,1200px);max-height:90vh;object-fit:contain;border-radius:12px;box-shadow:0 30px 80px #00000080}.seer-chat__lightbox-close[data-v-dc651545]{position:fixed;top:18px;right:22px;display:grid;place-items:center;width:40px;height:40px;color:#fff;font-size:26px;line-height:1;background:#ffffff24;border-radius:50%}.seer-chat__lightbox-close[data-v-dc651545]:not(:disabled):hover{background:#ffffff3d}@keyframes seer-chat__fade-in-dc651545{0%{opacity:0}to{opacity:1}}.seer-chat__error[data-v-dc651545],.seer-chat__handoff[data-v-dc651545]{margin:0 18px;padding:10px 12px;border-radius:14px}.seer-chat__error[data-v-dc651545]{color:#8a1f11;background:#fff0ed}.seer-chat__handoff[data-v-dc651545]{color:#704600;background:#fff7db}.seer-chat__form[data-v-dc651545]{display:grid;gap:11px;margin:0 18px 18px;padding:14px;background:#fff;border:1px solid rgba(127,143,170,.26);border-radius:22px;box-shadow:0 18px 38px #151f3417}.seer-chat__label[data-v-dc651545]{font-size:13px;font-weight:700}.seer-chat textarea[data-v-dc651545]{width:100%;box-sizing:border-box;min-height:92px;padding:14px;color:inherit;line-height:1.55;background:#f8fafd;border:1px solid transparent;border-radius:16px;outline:none;resize:vertical;transition:background .16s ease,border-color .16s ease,box-shadow .16s ease}.seer-chat button[data-v-dc651545]{color:inherit;font-weight:700;border:0;cursor:pointer;transition:transform .16s ease,box-shadow .16s ease,opacity .16s ease}.seer-chat button[data-v-dc651545]:not(:disabled):hover{transform:translateY(-1px)}.seer-chat button[data-v-dc651545]:disabled{cursor:not-allowed;opacity:.5}.seer-chat button[data-v-dc651545]:focus-visible,.seer-chat textarea[data-v-dc651545]:focus-visible{border-color:#1f5eff6b;box-shadow:0 0 0 4px #1f5eff21}.seer-chat__clear-button[data-v-dc651545]{flex:0 0 auto;padding:8px 11px;color:#ffffffc7;font-size:12px;background:transparent;border:1px solid rgba(255,255,255,.18)!important;border-radius:12px}.seer-chat__clear-button[data-v-dc651545]:not(:disabled):hover{color:#fff;background:#ffffff1a;box-shadow:none}.seer-chat__stop-button[data-v-dc651545]{padding:8px 11px;color:#6c3b12;font-size:12px;background:#fff1df;border-radius:12px}.seer-chat__send-button[data-v-dc651545]{display:inline-flex;gap:8px;align-items:center;justify-content:center;min-width:86px;padding:10px 14px;color:#fff;background:linear-gradient(135deg,#1f5eff,#173fb3);border-radius:14px;box-shadow:0 12px 24px #1f5eff38}.seer-chat__send-button[data-v-dc651545]:not(:disabled):hover{box-shadow:0 14px 28px #1f5eff47}.seer-chat__hint[data-v-dc651545]{margin-right:auto;color:#7a879a;font-size:12px}.seer-chat__composer-footer[data-v-dc651545]{display:flex;gap:8px;align-items:center;justify-content:flex-end}
1
+ .seer-chat[data-v-e6edd9a7]{color:#18212f;background:radial-gradient(circle at 18% 0,#476aff24,#0000 32%),linear-gradient(#fff 0%,#f7f9fd 100%);border:1px solid #7f8faa47;border-radius:28px;gap:18px;max-width:720px;min-height:640px;padding:0;display:grid;overflow:hidden;box-shadow:0 24px 70px #151f3429}.seer-chat__header[data-v-e6edd9a7]{color:#fff;background:linear-gradient(135deg,#141f34f5,#243f76f0),#172033;justify-content:space-between;align-items:flex-start;gap:16px;padding:22px 22px 18px;display:flex}.seer-chat__header h2[data-v-e6edd9a7]{letter-spacing:-.03em;margin:0;font-size:22px}.seer-chat__header p[data-v-e6edd9a7],.seer-chat__empty[data-v-e6edd9a7],.seer-chat__status[data-v-e6edd9a7]{color:#5f6f89;margin:4px 0 0;font-size:14px}.seer-chat__header p[data-v-e6edd9a7]{color:#ffffffb8;max-width:34ch}.seer-chat__messages[data-v-e6edd9a7]{align-content:start;gap:12px;min-height:330px;max-height:470px;padding:18px 18px 4px;display:grid;overflow-y:auto}.seer-chat__empty[data-v-e6edd9a7]{color:#66748a;text-align:center;background:#ffffff9e;border:1px dashed #7f8faa61;border-radius:22px;place-items:center;min-height:260px;padding:0 28px;display:grid}.seer-chat__message[data-v-e6edd9a7]{background:#ffffffe0;border:1px solid #7f8faa29;border-radius:18px 18px 18px 6px;gap:6px;max-width:min(86%,520px);padding:13px 15px;display:grid;box-shadow:0 12px 28px #151f3414}.seer-chat__message--user[data-v-e6edd9a7]{color:#fff;background:linear-gradient(135deg,#285cff,#1d45c9);border-color:#0000;border-radius:18px 18px 6px;justify-self:end;box-shadow:0 14px 30px #1f5eff3d}.seer-chat__role[data-v-e6edd9a7]{opacity:.75;font-size:12px;font-weight:700}.seer-chat__body[data-v-e6edd9a7]{flex-direction:column;display:flex}.seer-chat__line[data-v-e6edd9a7]{white-space:pre-wrap;overflow-wrap:anywhere;margin:0}.seer-chat__line[data-v-e6edd9a7]:not(:last-child){margin-bottom:5px}.seer-chat__line[data-v-e6edd9a7]:empty{height:7px}.seer-chat__strong[data-v-e6edd9a7]{font-weight:700}.seer-chat__price[data-v-e6edd9a7]{color:#e0331d;font-weight:600}.seer-chat__message--user .seer-chat__price[data-v-e6edd9a7]{color:#ffe2dc}.seer-chat__link[data-v-e6edd9a7]{color:#1f5eff;text-underline-offset:2px;overflow-wrap:anywhere;font-weight:600;text-decoration:underline}.seer-chat__link[data-v-e6edd9a7]:hover{color:#173fb3}.seer-chat__message--user .seer-chat__link[data-v-e6edd9a7]{color:#fff}.seer-chat__image-button[data-v-e6edd9a7]{cursor:zoom-in;background:0 0;border:0;padding:0;display:block}.seer-chat__image-button[data-v-e6edd9a7]:not(:disabled):hover{transform:none}.seer-chat__image[data-v-e6edd9a7]{border:1px solid #7f8faa38;border-radius:12px;max-width:100%;margin:6px 0;transition:filter .16s;display:block}.seer-chat__image-button:hover .seer-chat__image[data-v-e6edd9a7]{filter:brightness(.96)}.seer-chat__lightbox[data-v-e6edd9a7]{z-index:1000;cursor:zoom-out;background:#0a0f1ad1;place-items:center;padding:32px;animation:.14s seer-chat__fade-in-e6edd9a7;display:grid;position:fixed;inset:0}.seer-chat__lightbox-image[data-v-e6edd9a7]{object-fit:contain;border-radius:12px;max-width:min(92vw,1200px);max-height:90vh;box-shadow:0 30px 80px #00000080}.seer-chat__lightbox-close[data-v-e6edd9a7]{color:#fff;background:#ffffff24;border-radius:50%;place-items:center;width:40px;height:40px;font-size:26px;line-height:1;display:grid;position:fixed;top:18px;right:22px}.seer-chat__lightbox-close[data-v-e6edd9a7]:not(:disabled):hover{background:#ffffff3d}@keyframes seer-chat__fade-in-e6edd9a7{0%{opacity:0}to{opacity:1}}.seer-chat__error[data-v-e6edd9a7],.seer-chat__handoff[data-v-e6edd9a7]{border-radius:14px;margin:0 18px;padding:10px 12px}.seer-chat__error[data-v-e6edd9a7]{color:#8a1f11;background:#fff0ed}.seer-chat__handoff[data-v-e6edd9a7]{color:#704600;background:#fff7db}.seer-chat__form[data-v-e6edd9a7]{background:#fff;border:1px solid #7f8faa42;border-radius:22px;gap:11px;margin:0 18px 18px;padding:14px;display:grid;box-shadow:0 18px 38px #151f3417}.seer-chat__label[data-v-e6edd9a7]{font-size:13px;font-weight:700}.seer-chat textarea[data-v-e6edd9a7]{box-sizing:border-box;width:100%;min-height:92px;color:inherit;resize:vertical;background:#f8fafd;border:1px solid #0000;border-radius:16px;outline:none;padding:14px;line-height:1.55;transition:background .16s,border-color .16s,box-shadow .16s}.seer-chat button[data-v-e6edd9a7]{color:inherit;cursor:pointer;border:0;font-weight:700;transition:transform .16s,box-shadow .16s,opacity .16s}.seer-chat button[data-v-e6edd9a7]:not(:disabled):hover{transform:translateY(-1px)}.seer-chat button[data-v-e6edd9a7]:disabled{cursor:not-allowed;opacity:.5}.seer-chat button[data-v-e6edd9a7]:focus-visible,.seer-chat textarea[data-v-e6edd9a7]:focus-visible{border-color:#1f5eff6b;box-shadow:0 0 0 4px #1f5eff21}.seer-chat__clear-button[data-v-e6edd9a7]{color:#ffffffc7;background:0 0;border-radius:12px;flex:none;padding:8px 11px;font-size:12px;border:1px solid #ffffff2e!important}.seer-chat__clear-button[data-v-e6edd9a7]:not(:disabled):hover{color:#fff;box-shadow:none;background:#ffffff1a}.seer-chat__stop-button[data-v-e6edd9a7]{color:#6c3b12;background:#fff1df;border-radius:12px;padding:8px 11px;font-size:12px}.seer-chat__mic-button[data-v-e6edd9a7]{color:#285cff;background:#edf2ff;border-radius:50%;flex:none;place-items:center;width:40px;height:40px;padding:0;display:grid}.seer-chat__mic-button[data-v-e6edd9a7]:not(:disabled):hover{background:#dfe8ff}.seer-chat__mic-button--recording[data-v-e6edd9a7]{color:#fff;background:#d9342b;animation:1.4s ease-in-out infinite seer-chat__recording-pulse-e6edd9a7;box-shadow:0 0 0 5px #d9342b24}.seer-chat__mic-button--recording[data-v-e6edd9a7]:not(:disabled):hover{background:#bd271f}.seer-chat__stop-icon[data-v-e6edd9a7]{background:currentColor;border-radius:3px;width:12px;height:12px}@keyframes seer-chat__recording-pulse-e6edd9a7{50%{box-shadow:0 0 0 9px #d9342b14}}.seer-chat__send-button[data-v-e6edd9a7]{color:#fff;background:linear-gradient(135deg,#1f5eff,#173fb3);border-radius:14px;justify-content:center;align-items:center;gap:8px;min-width:86px;padding:10px 14px;display:inline-flex;box-shadow:0 12px 24px #1f5eff38}.seer-chat__send-button[data-v-e6edd9a7]:not(:disabled):hover{box-shadow:0 14px 28px #1f5eff47}.seer-chat__hint[data-v-e6edd9a7]{color:#7a879a;margin-right:auto;font-size:12px}.seer-chat__composer-footer[data-v-e6edd9a7]{justify-content:flex-end;align-items:center;gap:8px;display:flex}
2
+ /*$vite$:1*/
package/dist/index.d.ts CHANGED
@@ -3,6 +3,6 @@ export { linkAnchorLabel, toContentLines, toContentSegments, type ContentSegment
3
3
  export { SeerChatPlugin } from './plugin';
4
4
  export { useSeerChat } from './useSeerChat';
5
5
  export { default as SeerChat } from './components/SeerChat.vue';
6
- export type { SeerChatCallbacks, SeerChatMessage, SeerChatRequest, SeerChatResult, SeerChatRole, SeerClient, SeerClientOptions, } from './client';
6
+ export type { SeerChatCallbacks, SeerChatMessage, SeerChatRequest, SeerChatResult, SeerChatRole, SeerClient, SeerClientOptions, SeerTranscriptionCallbacks, SeerTranscriptionResult, } from './client';
7
7
  export type { SeerChatPluginOptions } from './plugin';
8
8
  export type { UseSeerChatOptions } from './useSeerChat';
package/dist/index.js CHANGED
@@ -1,407 +1,540 @@
1
- import { hasInjectionContext as K, inject as D, ref as y, defineComponent as U, computed as B, onBeforeUnmount as M, openBlock as h, createElementBlock as _, createElementVNode as f, toDisplayString as C, unref as g, createCommentVNode as T, Fragment as R, renderList as I, normalizeClass as q, withModifiers as A, withDirectives as z, withKeys as F, vModelText as V } from "vue";
2
- function X(e) {
3
- const n = e.fetcher ?? fetch, t = e.model ?? "seer", s = e.now ?? (() => Math.floor(Date.now() / 1e3));
4
- let r, a = 0;
5
- return {
6
- async chat(l, i = {}) {
7
- const c = l.stream ?? !1, o = await v(), m = {
8
- "Content-Type": "application/json"
9
- };
10
- o && (m.Authorization = `Bearer ${o}`);
11
- const k = await n(e.endpoint, {
12
- method: "POST",
13
- headers: m,
14
- signal: i.signal,
15
- body: JSON.stringify({
16
- model: l.model ?? t,
17
- messages: l.messages,
18
- stream: c,
19
- conversation_id: l.conversationId,
20
- metadata: l.metadata
21
- })
22
- });
23
- if (!k.ok)
24
- throw new Error(await P(k));
25
- if (c)
26
- return G(k, i.onDelta);
27
- const w = await k.json();
28
- return j(w);
29
- }
30
- };
31
- async function v() {
32
- if (!e.sessionEndpoint)
33
- return;
34
- const l = s();
35
- if (r && a - 30 > l)
36
- return r;
37
- const i = await n(e.sessionEndpoint, {
38
- method: "GET",
39
- headers: await p()
40
- });
41
- if (!i.ok)
42
- throw new Error(await P(i));
43
- const c = await i.json();
44
- return r = c.token, a = c.expires_at, r;
45
- }
46
- async function p() {
47
- const l = {};
48
- return e.challengeTokenProvider && (l["X-Seer-Challenge-Token"] = await e.challengeTokenProvider()), l;
49
- }
1
+ import { Fragment as e, computed as t, createCommentVNode as n, createElementBlock as r, createElementVNode as i, defineComponent as a, hasInjectionContext as o, inject as s, normalizeClass as c, onBeforeUnmount as l, openBlock as u, ref as d, renderList as f, toDisplayString as p, unref as m, vModelText as h, withDirectives as g, withKeys as _, withModifiers as v } from "vue";
2
+ //#region src/client.ts
3
+ function y(e) {
4
+ let t = e.fetcher ?? fetch, n = e.model ?? "seer", r = b(e), i = e.now ?? (() => Math.floor(Date.now() / 1e3)), a, o = 0;
5
+ return {
6
+ transcriptionEnabled: !!r,
7
+ async chat(r, i = {}) {
8
+ let a = r.stream ?? !1, o = await s(), c = { "Content-Type": "application/json" };
9
+ o && (c.Authorization = `Bearer ${o}`);
10
+ let l = await t(e.endpoint, {
11
+ method: "POST",
12
+ headers: c,
13
+ signal: i.signal,
14
+ body: JSON.stringify({
15
+ model: r.model ?? n,
16
+ messages: r.messages,
17
+ stream: a,
18
+ conversation_id: r.conversationId,
19
+ metadata: r.metadata
20
+ })
21
+ });
22
+ if (!l.ok) throw Error(await w(l));
23
+ return a ? S(l, i.onDelta) : x(await l.json());
24
+ },
25
+ async transcribe(e, n, i = {}) {
26
+ if (!r) throw Error("Seer audio transcription endpoint is not configured");
27
+ let a = await s(), o = {};
28
+ a && (o.Authorization = `Bearer ${a}`);
29
+ let c = new FormData();
30
+ c.append("file", e, n);
31
+ let l = await t(r, {
32
+ method: "POST",
33
+ headers: o,
34
+ signal: i.signal,
35
+ body: c
36
+ });
37
+ if (!l.ok) throw Error(await w(l));
38
+ let u = await l.json(), d = typeof u.text == "string" ? u.text.trim() : "";
39
+ if (!d) throw Error("Seer audio transcription returned empty text");
40
+ return { text: d };
41
+ }
42
+ };
43
+ async function s() {
44
+ if (!e.sessionEndpoint) return;
45
+ let n = i();
46
+ if (a && o - 30 > n) return a;
47
+ let r = await t(e.sessionEndpoint, {
48
+ method: "GET",
49
+ headers: await c()
50
+ });
51
+ if (!r.ok) throw Error(await w(r));
52
+ let s = await r.json();
53
+ return a = s.token, o = s.expires_at, a;
54
+ }
55
+ async function c() {
56
+ let t = {};
57
+ return e.challengeTokenProvider && (t["X-Seer-Challenge-Token"] = await e.challengeTokenProvider()), t;
58
+ }
50
59
  }
51
- function j(e) {
52
- var t, s, r;
53
- const n = (t = e.choices) == null ? void 0 : t[0];
54
- return {
55
- content: ((s = n == null ? void 0 : n.message) == null ? void 0 : s.content) ?? ((r = n == null ? void 0 : n.delta) == null ? void 0 : r.content) ?? "",
56
- conversationId: e.conversation_id,
57
- citations: e.citations ?? [],
58
- handoffRequested: e.handoff_requested ?? !1,
59
- finishReason: n == null ? void 0 : n.finish_reason
60
- };
60
+ function b(e) {
61
+ if (e.transcriptionEndpoint !== void 0) return e.transcriptionEndpoint || void 0;
62
+ if (e.endpoint.endsWith("/v1/browser/chat/completions")) return `${e.endpoint.slice(0, -28)}/v1/browser/audio/transcriptions`;
63
+ }
64
+ function x(e) {
65
+ let t = e.choices?.[0];
66
+ return {
67
+ content: t?.message?.content ?? t?.delta?.content ?? "",
68
+ conversationId: e.conversation_id,
69
+ citations: e.citations ?? [],
70
+ handoffRequested: e.handoff_requested ?? !1,
71
+ finishReason: t?.finish_reason
72
+ };
61
73
  }
62
- async function G(e, n) {
63
- var c, o, m;
64
- if (!e.body)
65
- return j({});
66
- const t = e.body.getReader(), s = new TextDecoder();
67
- let r = "", a = "", v, p = [], l = !1, i;
68
- for (; ; ) {
69
- const { done: k, value: w } = await t.read();
70
- if (k)
71
- break;
72
- r += s.decode(w, { stream: !0 });
73
- const x = r.split(`
74
-
75
- `);
76
- r = x.pop() ?? "";
77
- for (const u of x) {
78
- const E = H(u);
79
- if (!E || E === "[DONE]")
80
- continue;
81
- const b = JSON.parse(E), d = (c = b.choices) == null ? void 0 : c[0], S = ((o = d == null ? void 0 : d.delta) == null ? void 0 : o.content) ?? ((m = d == null ? void 0 : d.message) == null ? void 0 : m.content) ?? "";
82
- S && (a += S, n == null || n(S)), v = b.conversation_id ?? v, p = b.citations ?? p, l = b.handoff_requested ?? l, i = (d == null ? void 0 : d.finish_reason) ?? i;
83
- }
84
- }
85
- return {
86
- content: a,
87
- conversationId: v,
88
- citations: p,
89
- handoffRequested: l,
90
- finishReason: i
91
- };
74
+ async function S(e, t) {
75
+ if (!e.body) return x({});
76
+ let n = e.body.getReader(), r = new TextDecoder(), i = "", a = "", o, s = [], c = !1, l, u = !1;
77
+ for (;;) {
78
+ let { done: e, value: d } = await n.read();
79
+ if (e) break;
80
+ i += r.decode(d, { stream: !0 });
81
+ let f = i.split("\n\n");
82
+ i = f.pop() ?? "";
83
+ for (let e of f) {
84
+ let n = C(e);
85
+ if (!n) continue;
86
+ if (n === "[DONE]") {
87
+ u = !0;
88
+ continue;
89
+ }
90
+ let r = JSON.parse(n), i = r.choices?.[0], d = i?.delta?.content ?? i?.message?.content ?? "";
91
+ d && (a += d, t?.(d)), o = r.conversation_id ?? o, s = r.citations ?? s, c = r.handoff_requested ?? c, l = i?.finish_reason ?? l;
92
+ }
93
+ }
94
+ if (!u) throw Error("seer chat stream ended before [DONE]");
95
+ return {
96
+ content: a,
97
+ conversationId: o,
98
+ citations: s,
99
+ handoffRequested: c,
100
+ finishReason: l
101
+ };
92
102
  }
93
- function H(e) {
94
- const n = e.split(`
95
- `).map((t) => t.trim()).filter((t) => t.startsWith("data:")).map((t) => t.slice(5).trim());
96
- return n.length === 0 ? null : n.join(`
97
- `);
103
+ function C(e) {
104
+ let t = e.split("\n").map((e) => e.trim()).filter((e) => e.startsWith("data:")).map((e) => e.slice(5).trim());
105
+ return t.length === 0 ? null : t.join("\n");
98
106
  }
99
- async function P(e) {
100
- let n = e.statusText || "unknown error";
101
- try {
102
- const t = await e.json();
103
- typeof (t == null ? void 0 : t.detail) == "string" ? n = t.detail : typeof (t == null ? void 0 : t.error) == "string" && (n = t.error);
104
- } catch {
105
- const t = await e.text().catch(() => "");
106
- t && (n = t);
107
- }
108
- return `seer chat request failed with status ${e.status}: ${n}`;
107
+ async function w(e) {
108
+ let t = e.statusText || "unknown error";
109
+ try {
110
+ let n = await e.json();
111
+ typeof n?.detail == "string" ? t = n.detail : typeof n?.error == "string" && (t = n.error);
112
+ } catch {
113
+ let n = await e.text().catch(() => "");
114
+ n && (t = n);
115
+ }
116
+ return `seer chat request failed with status ${e.status}: ${t}`;
109
117
  }
110
- const O = /https?:\/\/[^\s)]+/g, J = /\.(png|jpe?g|gif|webp|bmp|svg)$/i, W = "单价\\s*\\d[\\d.]*\\s*[A-Za-z]+\\/kg", Y = "按当前信息为您筛选出[^\\n:]*推荐方案[^\\n:]*:", Z = "方案PK|时效最快|价格最低", $ = new RegExp(`(${W})|(${Y})|(${Z})`, "g");
111
- function L(e) {
112
- const n = [];
113
- $.lastIndex = 0;
114
- let t = 0, s;
115
- for (; (s = $.exec(e)) !== null; ) {
116
- s.index > t && n.push({ kind: "text", value: e.slice(t, s.index) });
117
- const r = s[0];
118
- n.push(s[1] ? { kind: "price", value: r } : { kind: "strong", value: r }), t = s.index + r.length;
119
- }
120
- return t < e.length && n.push({ kind: "text", value: e.slice(t) }), n;
118
+ //#endregion
119
+ //#region src/content.ts
120
+ var T = /https?:\/\/[^\s)]+/g, E = /\.(png|jpe?g|gif|webp|bmp|svg)$/i, D = /* @__PURE__ */ RegExp("(单价\\s*\\d[\\d.]*\\s*[A-Za-z]+\\/kg)|(按当前信息为您筛选出[^\\n:]*推荐方案[^\\n:]*:)|(方案PK|时效最快|价格最低)", "g");
121
+ function O(e) {
122
+ let t = [];
123
+ D.lastIndex = 0;
124
+ let n = 0, r;
125
+ for (; (r = D.exec(e)) !== null;) {
126
+ r.index > n && t.push({
127
+ kind: "text",
128
+ value: e.slice(n, r.index)
129
+ });
130
+ let i = r[0];
131
+ t.push(r[1] ? {
132
+ kind: "price",
133
+ value: i
134
+ } : {
135
+ kind: "strong",
136
+ value: i
137
+ }), n = r.index + i.length;
138
+ }
139
+ return n < e.length && t.push({
140
+ kind: "text",
141
+ value: e.slice(n)
142
+ }), t;
121
143
  }
122
- const Q = /标签|箱唛|唛头|入库单|交接单|入仓单|面单|下载/;
123
- function ee(e, n) {
124
- if (/方案|截图|报价图|效果图|示意图|图片/.test(n))
125
- return !0;
126
- if (Q.test(n))
127
- return !1;
128
- const t = e.split(/[?#]/)[0];
129
- return J.test(t);
144
+ var k = /标签|箱唛|唛头|入库单|交接单|入仓单|面单|下载/;
145
+ function A(e, t) {
146
+ if (/方案|截图|报价图|效果图|示意图|图片/.test(t)) return !0;
147
+ if (k.test(t)) return !1;
148
+ let n = e.split(/[?#]/)[0];
149
+ return E.test(n);
130
150
  }
131
- function te(e) {
132
- return /方案|报价|截图|PK/.test(e) ? "方案PK" : "图片";
151
+ function j(e) {
152
+ return /方案|报价|截图|PK/.test(e) ? "方案PK" : "图片";
133
153
  }
134
- function ne(e) {
135
- return /标签|箱唛/.test(e) ? "点击下载箱唛" : /入库单|交接单/.test(e) ? "点击下载入库单" : /面单/.test(e) ? "点击下载面单" : /截图|方案/.test(e) ? "点击查看方案PK" : /下载链接|labelUrl|下载/.test(e) ? "点击下载" : /链接/.test(e) ? "点击查看" : null;
154
+ function M(e) {
155
+ return /标签|箱唛/.test(e) ? "点击下载箱唛" : /入库单|交接单/.test(e) ? "点击下载入库单" : /面单/.test(e) ? "点击下载面单" : /截图|方案/.test(e) ? "点击查看方案PK" : /下载链接|labelUrl|下载/.test(e) ? "点击下载" : /链接/.test(e) ? "点击查看" : null;
136
156
  }
137
- function se(e) {
138
- const n = [];
139
- O.lastIndex = 0;
140
- let t = 0, s;
141
- for (; (s = O.exec(e)) !== null; ) {
142
- const r = s[0], a = s.index, v = e.lastIndexOf(`
143
- `, a - 1) + 1, p = e.slice(v, a), l = ee(r, p);
144
- if (a > t) {
145
- let i = e.slice(t, a);
146
- l && (i = i.replace(/链接([::])\s*$/, "$1")), n.push(...L(i));
147
- }
148
- l ? n.push({ kind: "image", href: r, alt: te(p) }) : n.push({ kind: "link", href: r, label: ne(p) ?? r }), t = a + r.length;
149
- }
150
- return t < e.length && n.push(...L(e.slice(t))), n;
157
+ function N(e) {
158
+ let t = [];
159
+ T.lastIndex = 0;
160
+ let n = 0, r;
161
+ for (; (r = T.exec(e)) !== null;) {
162
+ let i = r[0], a = r.index, o = e.lastIndexOf("\n", a - 1) + 1, s = e.slice(o, a), c = A(i, s);
163
+ if (a > n) {
164
+ let r = e.slice(n, a);
165
+ c && (r = r.replace(/链接([::])\s*$/, "$1")), t.push(...O(r));
166
+ }
167
+ c ? t.push({
168
+ kind: "image",
169
+ href: i,
170
+ alt: j(s)
171
+ }) : t.push({
172
+ kind: "link",
173
+ href: i,
174
+ label: M(s) ?? i
175
+ }), n = a + i.length;
176
+ }
177
+ return n < e.length && t.push(...O(e.slice(n))), t;
151
178
  }
152
- function ae(e) {
153
- return e.split(`
154
- `).map((n) => se(n));
179
+ function P(e) {
180
+ return e.split("\n").map((e) => N(e));
155
181
  }
156
- const N = Symbol("seer-chat-options"), Ie = {
157
- install(e, n) {
158
- e.provide(N, n);
159
- }
160
- };
161
- function oe(e = {}) {
162
- const t = { ...K() ? D(N, void 0) : void 0, ...e };
163
- if (!t.endpoint)
164
- throw new Error("Seer chat endpoint is required. Pass it to app.use() or useSeerChat().");
165
- const s = {
166
- endpoint: t.endpoint,
167
- sessionEndpoint: t.sessionEndpoint,
168
- challengeTokenProvider: t.challengeTokenProvider,
169
- model: t.model,
170
- fetcher: t.fetcher
171
- }, r = X(s), a = y([]), v = y(!1), p = y(null), l = y(), i = y([]), c = y(!1);
172
- let o = null;
173
- async function m(x) {
174
- const u = x.trim();
175
- if (!u || v.value)
176
- return;
177
- const E = { role: "user", content: u };
178
- a.value = [...a.value, E], v.value = !0, p.value = null, o = new AbortController();
179
- const b = { role: "assistant", content: "" };
180
- t.stream && (a.value = [...a.value, b]);
181
- try {
182
- const d = await r.chat(
183
- {
184
- messages: a.value.filter((S) => S.content),
185
- stream: t.stream ?? !1,
186
- conversationId: l.value
187
- },
188
- {
189
- signal: o.signal,
190
- onDelta(S) {
191
- b.content += S, a.value = [...a.value];
192
- }
193
- }
194
- );
195
- t.stream || (a.value = [...a.value, { role: "assistant", content: d.content }]), l.value = d.conversationId, i.value = d.citations, c.value = d.handoffRequested;
196
- } catch (d) {
197
- d.name !== "AbortError" && (p.value = d instanceof Error ? d.message : String(d));
198
- } finally {
199
- v.value = !1, o = null;
200
- }
201
- }
202
- function k() {
203
- a.value = [], p.value = null, l.value = void 0, i.value = [], c.value = !1;
204
- }
205
- function w() {
206
- o == null || o.abort();
207
- }
208
- return {
209
- messages: a,
210
- loading: v,
211
- error: p,
212
- conversationId: l,
213
- citations: i,
214
- handoffRequested: c,
215
- send: m,
216
- reset: k,
217
- abort: w
218
- };
182
+ //#endregion
183
+ //#region src/plugin.ts
184
+ var F = Symbol("seer-chat-options"), I = { install(e, t) {
185
+ e.provide(F, t);
186
+ } };
187
+ //#endregion
188
+ //#region src/useSeerChat.ts
189
+ function L(e = {}) {
190
+ let t = o() ? s(F, void 0) : void 0, n = Object.fromEntries(Object.entries(e).filter(([, e]) => e !== void 0)), r = {
191
+ ...t,
192
+ ...n
193
+ };
194
+ if (!r.endpoint) throw Error("Seer chat endpoint is required. Pass it to app.use() or useSeerChat().");
195
+ let i = y({
196
+ endpoint: r.endpoint,
197
+ sessionEndpoint: r.sessionEndpoint,
198
+ transcriptionEndpoint: r.transcriptionEndpoint,
199
+ challengeTokenProvider: r.challengeTokenProvider,
200
+ model: r.model,
201
+ fetcher: r.fetcher
202
+ }), a = d([]), c = d(!1), l = d(null), u = d(), f = d([]), p = d(!1), m = r.voiceEnabled !== !1 && i.transcriptionEnabled, h = r.maxRecordingSeconds ?? 60, g = null;
203
+ async function _(e) {
204
+ let t = e.trim();
205
+ if (!t || c.value) return;
206
+ let n = {
207
+ role: "user",
208
+ content: t
209
+ };
210
+ a.value = [...a.value, n], c.value = !0, l.value = null, g = new AbortController();
211
+ let o = {
212
+ role: "assistant",
213
+ content: ""
214
+ };
215
+ r.stream && (a.value = [...a.value, o]);
216
+ try {
217
+ let e = await i.chat({
218
+ messages: a.value.filter((e) => e.content),
219
+ stream: r.stream ?? !1,
220
+ conversationId: u.value
221
+ }, {
222
+ signal: g.signal,
223
+ onDelta(e) {
224
+ o.content += e, a.value = [...a.value];
225
+ }
226
+ });
227
+ r.stream || (a.value = [...a.value, {
228
+ role: "assistant",
229
+ content: e.content
230
+ }]), u.value = e.conversationId, f.value = e.citations, p.value = e.handoffRequested;
231
+ } catch (e) {
232
+ e.name !== "AbortError" && (l.value = e instanceof Error ? e.message : String(e));
233
+ } finally {
234
+ c.value = !1, g = null;
235
+ }
236
+ }
237
+ function v() {
238
+ a.value = [], l.value = null, u.value = void 0, f.value = [], p.value = !1;
239
+ }
240
+ function b() {
241
+ g?.abort();
242
+ }
243
+ return {
244
+ messages: a,
245
+ loading: c,
246
+ error: l,
247
+ conversationId: u,
248
+ citations: f,
249
+ handoffRequested: p,
250
+ transcriptionEnabled: m,
251
+ maxRecordingSeconds: h,
252
+ transcribe: i.transcribe,
253
+ send: _,
254
+ reset: v,
255
+ abort: b
256
+ };
219
257
  }
220
- const re = {
221
- class: "seer-chat",
222
- "aria-label": "多发货 AI 客服聊天"
223
- }, le = { class: "seer-chat__header" }, ie = {
224
- class: "seer-chat__messages",
225
- "aria-live": "polite"
226
- }, ce = {
227
- key: 0,
228
- class: "seer-chat__empty"
229
- }, ue = { class: "seer-chat__role" }, de = { class: "seer-chat__body" }, fe = ["href"], he = ["aria-label", "onClick"], _e = ["src", "alt"], ve = {
230
- key: 2,
231
- class: "seer-chat__strong"
232
- }, pe = {
233
- key: 3,
234
- class: "seer-chat__price"
235
- }, me = { key: 4 }, ge = {
236
- key: 0,
237
- class: "seer-chat__status"
238
- }, ke = {
239
- key: 1,
240
- class: "seer-chat__error",
241
- role: "alert"
242
- }, be = {
243
- key: 2,
244
- class: "seer-chat__handoff"
245
- }, ye = ["placeholder", "disabled", "onKeydown"], we = { class: "seer-chat__composer-footer" }, Ee = ["disabled"], Se = ["aria-label"], Ce = ["src", "alt"], xe = /* @__PURE__ */ U({
246
- __name: "SeerChat",
247
- props: {
248
- endpoint: {},
249
- model: { default: "seer" },
250
- stream: { type: Boolean, default: !1 },
251
- fetcher: {},
252
- title: { default: "多发货 AI 客服" },
253
- placeholder: { default: "输入你的问题,例如:美国海派怎么报价?" }
254
- },
255
- setup(e) {
256
- const n = e, t = y(""), s = oe({
257
- endpoint: n.endpoint,
258
- model: n.model,
259
- stream: n.stream,
260
- fetcher: n.fetcher
261
- }), r = B(
262
- () => t.value.trim().length > 0 && !s.loading.value
263
- ), a = y(null);
264
- function v(c) {
265
- c.key === "Escape" && l();
266
- }
267
- function p(c, o) {
268
- a.value = { src: c, alt: o }, window.addEventListener("keydown", v);
269
- }
270
- function l() {
271
- a.value = null, window.removeEventListener("keydown", v);
272
- }
273
- M(() => {
274
- window.removeEventListener("keydown", v);
275
- });
276
- async function i() {
277
- if (!r.value)
278
- return;
279
- const c = t.value;
280
- t.value = "", await s.send(c);
281
- }
282
- return (c, o) => (h(), _("section", re, [
283
- f("header", le, [
284
- f("div", null, [
285
- f("h2", null, C(e.title), 1),
286
- o[3] || (o[3] = f("p", null, "可咨询渠道报价、时效、下单资料、入仓和售后问题。", -1))
287
- ]),
288
- f("button", {
289
- type: "button",
290
- class: "seer-chat__clear-button",
291
- onClick: o[0] || (o[0] = (m) => g(s).reset())
292
- }, " 清空对话 ")
293
- ]),
294
- f("div", ie, [
295
- g(s).messages.value.length === 0 ? (h(), _("p", ce, " 直接输入问题即可,复杂查单或赔付问题会提示转人工。 ")) : T("", !0),
296
- (h(!0), _(R, null, I(g(s).messages.value, (m, k) => (h(), _("article", {
297
- key: `${m.role}-${k}`,
298
- class: q(["seer-chat__message", `seer-chat__message--${m.role}`])
299
- }, [
300
- f("span", ue, C(m.role === "user" ? "你" : "AI 客服"), 1),
301
- f("div", de, [
302
- (h(!0), _(R, null, I(g(ae)(m.content), (w, x) => (h(), _("p", {
303
- key: x,
304
- class: "seer-chat__line"
305
- }, [
306
- (h(!0), _(R, null, I(w, (u, E) => (h(), _(R, { key: E }, [
307
- u.kind === "link" ? (h(), _("a", {
308
- key: 0,
309
- class: "seer-chat__link",
310
- href: u.href,
311
- target: "_blank",
312
- rel: "noopener noreferrer"
313
- }, C(u.label), 9, fe)) : u.kind === "image" ? (h(), _("button", {
314
- key: 1,
315
- type: "button",
316
- class: "seer-chat__image-button",
317
- "aria-label": `放大查看${u.alt}`,
318
- onClick: (b) => p(u.href, u.alt)
319
- }, [
320
- f("img", {
321
- class: "seer-chat__image",
322
- src: u.href,
323
- alt: u.alt,
324
- loading: "lazy"
325
- }, null, 8, _e)
326
- ], 8, he)) : u.kind === "strong" ? (h(), _("strong", ve, C(u.value), 1)) : u.kind === "price" ? (h(), _("span", pe, C(u.value), 1)) : (h(), _("span", me, C(u.value), 1))
327
- ], 64))), 128))
328
- ]))), 128))
329
- ])
330
- ], 2))), 128))
331
- ]),
332
- g(s).loading.value ? (h(), _("p", ge, "正在回复...")) : T("", !0),
333
- g(s).error.value ? (h(), _("p", ke, C(g(s).error.value), 1)) : T("", !0),
334
- g(s).handoffRequested.value ? (h(), _("p", be, " 建议转人工:请留下联系方式或等待客服接入。 ")) : T("", !0),
335
- f("form", {
336
- class: "seer-chat__form",
337
- onSubmit: A(i, ["prevent"])
338
- }, [
339
- o[6] || (o[6] = f("label", {
340
- class: "seer-chat__label",
341
- for: "seer-chat-input"
342
- }, "问题", -1)),
343
- z(f("textarea", {
344
- id: "seer-chat-input",
345
- "onUpdate:modelValue": o[1] || (o[1] = (m) => t.value = m),
346
- placeholder: e.placeholder,
347
- disabled: g(s).loading.value,
348
- rows: "3",
349
- onKeydown: F(A(i, ["exact", "prevent"]), ["enter"])
350
- }, null, 40, ye), [
351
- [V, t.value]
352
- ]),
353
- f("div", we, [
354
- o[5] || (o[5] = f("span", { class: "seer-chat__hint" }, "Enter 发送 · Shift+Enter 换行", -1)),
355
- g(s).loading.value ? (h(), _("button", {
356
- key: 0,
357
- type: "button",
358
- class: "seer-chat__stop-button",
359
- onClick: o[2] || (o[2] = (m) => g(s).abort())
360
- }, " 停止 ")) : T("", !0),
361
- f("button", {
362
- class: "seer-chat__send-button",
363
- type: "submit",
364
- disabled: !r.value
365
- }, [...o[4] || (o[4] = [
366
- f("span", null, "发送", -1),
367
- f("span", { "aria-hidden": "true" }, "↵", -1)
368
- ])], 8, Ee)
369
- ])
370
- ], 32),
371
- a.value ? (h(), _("div", {
372
- key: 3,
373
- class: "seer-chat__lightbox",
374
- role: "dialog",
375
- "aria-modal": "true",
376
- "aria-label": a.value.alt,
377
- onClick: l
378
- }, [
379
- f("button", {
380
- type: "button",
381
- class: "seer-chat__lightbox-close",
382
- "aria-label": "关闭",
383
- onClick: l
384
- }, " × "),
385
- f("img", {
386
- class: "seer-chat__lightbox-image",
387
- src: a.value.src,
388
- alt: a.value.alt
389
- }, null, 8, Ce)
390
- ], 8, Se)) : T("", !0)
391
- ]));
392
- }
393
- }), Te = (e, n) => {
394
- const t = e.__vccOpts || e;
395
- for (const [s, r] of n)
396
- t[s] = r;
397
- return t;
398
- }, Ae = /* @__PURE__ */ Te(xe, [["__scopeId", "data-v-dc651545"]]);
399
- export {
400
- Ae as SeerChat,
401
- Ie as SeerChatPlugin,
402
- X as createSeerClient,
403
- ne as linkAnchorLabel,
404
- ae as toContentLines,
405
- se as toContentSegments,
406
- oe as useSeerChat
407
- };
258
+ //#endregion
259
+ //#region src/components/SeerChat.vue?vue&type=script&setup=true&lang.ts
260
+ var ee = {
261
+ class: "seer-chat",
262
+ "aria-label": "多发货 AI 客服聊天"
263
+ }, te = { class: "seer-chat__header" }, ne = {
264
+ class: "seer-chat__messages",
265
+ "aria-live": "polite"
266
+ }, re = {
267
+ key: 0,
268
+ class: "seer-chat__empty"
269
+ }, ie = { class: "seer-chat__role" }, R = { class: "seer-chat__body" }, z = ["href"], B = ["aria-label", "onClick"], V = ["src", "alt"], H = {
270
+ key: 2,
271
+ class: "seer-chat__strong"
272
+ }, U = {
273
+ key: 3,
274
+ class: "seer-chat__price"
275
+ }, W = { key: 4 }, G = {
276
+ key: 0,
277
+ class: "seer-chat__status"
278
+ }, K = {
279
+ key: 1,
280
+ class: "seer-chat__error",
281
+ role: "alert"
282
+ }, q = {
283
+ key: 2,
284
+ class: "seer-chat__handoff"
285
+ }, J = [
286
+ "placeholder",
287
+ "disabled",
288
+ "onKeydown"
289
+ ], ae = { class: "seer-chat__composer-footer" }, oe = {
290
+ class: "seer-chat__hint",
291
+ "aria-live": "polite"
292
+ }, se = [
293
+ "disabled",
294
+ "aria-label",
295
+ "title"
296
+ ], ce = {
297
+ key: 0,
298
+ class: "seer-chat__stop-icon",
299
+ "aria-hidden": "true"
300
+ }, le = {
301
+ key: 1,
302
+ viewBox: "0 0 24 24",
303
+ width: "20",
304
+ height: "20",
305
+ "aria-hidden": "true"
306
+ }, ue = ["disabled"], de = ["aria-label"], fe = ["src", "alt"], Y = /*#__PURE__*/ ((e, t) => {
307
+ let n = e.__vccOpts || e;
308
+ for (let [e, r] of t) n[e] = r;
309
+ return n;
310
+ })(/* @__PURE__ */ a({
311
+ __name: "SeerChat",
312
+ props: {
313
+ endpoint: {},
314
+ sessionEndpoint: {},
315
+ transcriptionEndpoint: {},
316
+ model: {},
317
+ stream: {
318
+ type: Boolean,
319
+ default: void 0
320
+ },
321
+ fetcher: {},
322
+ title: { default: "多发货 AI 客服" },
323
+ placeholder: { default: "输入你的问题,例如:美国海派怎么报价?" },
324
+ voiceEnabled: {
325
+ type: Boolean,
326
+ default: void 0
327
+ },
328
+ maxRecordingSeconds: {}
329
+ },
330
+ setup(a) {
331
+ let o = a, s = d(""), y = d(!1), b = d(!1), x = d(null), S = L({
332
+ endpoint: o.endpoint,
333
+ sessionEndpoint: o.sessionEndpoint,
334
+ transcriptionEndpoint: o.transcriptionEndpoint,
335
+ model: o.model,
336
+ stream: o.stream,
337
+ fetcher: o.fetcher,
338
+ voiceEnabled: o.voiceEnabled,
339
+ maxRecordingSeconds: o.maxRecordingSeconds
340
+ }), C = t(() => s.value.trim().length > 0 && !S.loading.value && !y.value && !b.value), w = t(() => S.transcriptionEnabled && typeof navigator < "u" && typeof navigator.mediaDevices?.getUserMedia == "function" && typeof MediaRecorder < "u"), T = null, E = null, D = [], O, k = !1, A = d(null);
341
+ function j(e) {
342
+ e.key === "Escape" && N();
343
+ }
344
+ function M(e, t) {
345
+ A.value = {
346
+ src: e,
347
+ alt: t
348
+ }, window.addEventListener("keydown", j);
349
+ }
350
+ function N() {
351
+ A.value = null, window.removeEventListener("keydown", j);
352
+ }
353
+ l(() => {
354
+ k = !0, window.removeEventListener("keydown", j), Q(), T && T.state !== "inactive" && (T.ondataavailable = null, T.onstop = null, T.onerror = null, T.stop()), $();
355
+ });
356
+ async function F() {
357
+ if (!C.value) return;
358
+ let e = s.value;
359
+ s.value = "", await S.send(e);
360
+ }
361
+ async function I() {
362
+ if (y.value) {
363
+ X();
364
+ return;
365
+ }
366
+ await Y();
367
+ }
368
+ async function Y() {
369
+ if (!(!w.value || b.value || S.loading.value)) {
370
+ x.value = null;
371
+ try {
372
+ if (E = await navigator.mediaDevices.getUserMedia({
373
+ audio: {
374
+ echoCancellation: !0,
375
+ noiseSuppression: !0
376
+ },
377
+ video: !1
378
+ }), k) {
379
+ $();
380
+ return;
381
+ }
382
+ let e = pe();
383
+ T = e ? new MediaRecorder(E, {
384
+ mimeType: e,
385
+ audioBitsPerSecond: 64e3
386
+ }) : new MediaRecorder(E), D = [], T.ondataavailable = (e) => {
387
+ e.data.size > 0 && D.push(e.data);
388
+ }, T.onstop = () => {
389
+ Z(T?.mimeType || e || "audio/webm");
390
+ }, T.onerror = () => {
391
+ x.value = "录音失败,请重试或用文字输入。", y.value = !1, b.value = !1, Q(), $();
392
+ }, T.start(), y.value = !0, O = setTimeout(X, Math.max(1, S.maxRecordingSeconds) * 1e3);
393
+ } catch (e) {
394
+ x.value = he(e), $();
395
+ }
396
+ }
397
+ }
398
+ function X() {
399
+ !T || T.state === "inactive" || (y.value = !1, b.value = !0, Q(), T.stop());
400
+ }
401
+ async function Z(e) {
402
+ Q(), $();
403
+ let t = D;
404
+ if (D = [], T = null, k) return;
405
+ let n = new Blob(t, { type: e });
406
+ if (n.size === 0) {
407
+ x.value = "没有录到声音,请重试。", b.value = !1;
408
+ return;
409
+ }
410
+ try {
411
+ let t = await S.transcribe(n, me(e));
412
+ s.value = s.value.trim() ? `${s.value.trim()} ${t.text}` : t.text;
413
+ } catch {
414
+ x.value = "语音识别失败,请重试或用文字输入。";
415
+ } finally {
416
+ b.value = !1;
417
+ }
418
+ }
419
+ function pe() {
420
+ if (typeof MediaRecorder.isTypeSupported == "function") return [
421
+ "audio/webm;codecs=opus",
422
+ "audio/mp4",
423
+ "audio/ogg;codecs=opus",
424
+ "audio/webm"
425
+ ].find((e) => MediaRecorder.isTypeSupported(e));
426
+ }
427
+ function me(e) {
428
+ return `recording.${{
429
+ "audio/mp4": "m4a",
430
+ "audio/ogg": "ogg",
431
+ "audio/webm": "webm"
432
+ }[e.split(";", 1)[0].toLowerCase()] || "webm"}`;
433
+ }
434
+ function he(e) {
435
+ return e instanceof DOMException && e.name === "NotAllowedError" ? "请允许浏览器使用麦克风。" : e instanceof DOMException && e.name === "NotFoundError" ? "未检测到可用的麦克风。" : "无法启动麦克风,请重试或用文字输入。";
436
+ }
437
+ function Q() {
438
+ O !== void 0 && (clearTimeout(O), O = void 0);
439
+ }
440
+ function $() {
441
+ E?.getTracks().forEach((e) => e.stop()), E = null;
442
+ }
443
+ return (t, o) => (u(), r("section", ee, [
444
+ i("header", te, [i("div", null, [i("h2", null, p(a.title), 1), o[3] ||= i("p", null, "可咨询渠道报价、时效、下单资料、入仓和售后问题。", -1)]), i("button", {
445
+ type: "button",
446
+ class: "seer-chat__clear-button",
447
+ onClick: o[0] ||= (e) => m(S).reset()
448
+ }, " 清空对话 ")]),
449
+ i("div", ne, [m(S).messages.value.length === 0 ? (u(), r("p", re, " 直接输入问题即可,复杂查单或赔付问题会提示转人工。 ")) : n("", !0), (u(!0), r(e, null, f(m(S).messages.value, (t, n) => (u(), r("article", {
450
+ key: `${t.role}-${n}`,
451
+ class: c(["seer-chat__message", `seer-chat__message--${t.role}`])
452
+ }, [i("span", ie, p(t.role === "user" ? "你" : "AI 客服"), 1), i("div", R, [(u(!0), r(e, null, f(m(P)(t.content), (t, n) => (u(), r("p", {
453
+ key: n,
454
+ class: "seer-chat__line"
455
+ }, [(u(!0), r(e, null, f(t, (t, n) => (u(), r(e, { key: n }, [t.kind === "link" ? (u(), r("a", {
456
+ key: 0,
457
+ class: "seer-chat__link",
458
+ href: t.href,
459
+ target: "_blank",
460
+ rel: "noopener noreferrer"
461
+ }, p(t.label), 9, z)) : t.kind === "image" ? (u(), r("button", {
462
+ key: 1,
463
+ type: "button",
464
+ class: "seer-chat__image-button",
465
+ "aria-label": `放大查看${t.alt}`,
466
+ onClick: (e) => M(t.href, t.alt)
467
+ }, [i("img", {
468
+ class: "seer-chat__image",
469
+ src: t.href,
470
+ alt: t.alt,
471
+ loading: "lazy"
472
+ }, null, 8, V)], 8, B)) : t.kind === "strong" ? (u(), r("strong", H, p(t.value), 1)) : t.kind === "price" ? (u(), r("span", U, p(t.value), 1)) : (u(), r("span", W, p(t.value), 1))], 64))), 128))]))), 128))])], 2))), 128))]),
473
+ m(S).loading.value ? (u(), r("p", G, "正在回复...")) : n("", !0),
474
+ m(S).error.value || x.value ? (u(), r("p", K, p(x.value || m(S).error.value), 1)) : n("", !0),
475
+ m(S).handoffRequested.value ? (u(), r("p", q, " 建议转人工:请留下联系方式或等待客服接入。 ")) : n("", !0),
476
+ i("form", {
477
+ class: "seer-chat__form",
478
+ onSubmit: v(F, ["prevent"])
479
+ }, [
480
+ o[6] ||= i("label", {
481
+ class: "seer-chat__label",
482
+ for: "seer-chat-input"
483
+ }, "问题", -1),
484
+ g(i("textarea", {
485
+ id: "seer-chat-input",
486
+ "onUpdate:modelValue": o[1] ||= (e) => s.value = e,
487
+ placeholder: a.placeholder,
488
+ disabled: m(S).loading.value || b.value,
489
+ rows: "3",
490
+ onKeydown: _(v(F, ["exact", "prevent"]), ["enter"])
491
+ }, null, 40, J), [[h, s.value]]),
492
+ i("div", ae, [
493
+ i("span", oe, p(y.value ? `正在录音,最长 ${m(S).maxRecordingSeconds} 秒` : b.value ? "正在识别语音..." : "Enter 发送 · Shift+Enter 换行"), 1),
494
+ w.value ? (u(), r("button", {
495
+ key: 0,
496
+ type: "button",
497
+ class: c(["seer-chat__mic-button", { "seer-chat__mic-button--recording": y.value }]),
498
+ disabled: b.value || m(S).loading.value && !y.value,
499
+ "aria-label": y.value ? "停止录音" : "开始语音输入",
500
+ title: y.value ? "停止录音" : "语音输入",
501
+ onClick: I
502
+ }, [y.value ? (u(), r("span", ce)) : (u(), r("svg", le, [...o[4] ||= [i("path", {
503
+ fill: "currentColor",
504
+ d: "M12 14a3 3 0 0 0 3-3V5a3 3 0 1 0-6 0v6a3 3 0 0 0 3 3Zm5-3a1 1 0 1 1 2 0 7 7 0 0 1-6 6.93V20h3a1 1 0 1 1 0 2H8a1 1 0 1 1 0-2h3v-2.07A7 7 0 0 1 5 11a1 1 0 1 1 2 0 5 5 0 0 0 10 0Z"
505
+ }, null, -1)]]))], 10, se)) : n("", !0),
506
+ m(S).loading.value ? (u(), r("button", {
507
+ key: 1,
508
+ type: "button",
509
+ class: "seer-chat__stop-button",
510
+ onClick: o[2] ||= (e) => m(S).abort()
511
+ }, " 停止 ")) : n("", !0),
512
+ i("button", {
513
+ class: "seer-chat__send-button",
514
+ type: "submit",
515
+ disabled: !C.value
516
+ }, [...o[5] ||= [i("span", null, "发送", -1), i("span", { "aria-hidden": "true" }, "↵", -1)]], 8, ue)
517
+ ])
518
+ ], 32),
519
+ A.value ? (u(), r("div", {
520
+ key: 3,
521
+ class: "seer-chat__lightbox",
522
+ role: "dialog",
523
+ "aria-modal": "true",
524
+ "aria-label": A.value.alt,
525
+ onClick: N
526
+ }, [i("button", {
527
+ type: "button",
528
+ class: "seer-chat__lightbox-close",
529
+ "aria-label": "关闭",
530
+ onClick: N
531
+ }, " × "), i("img", {
532
+ class: "seer-chat__lightbox-image",
533
+ src: A.value.src,
534
+ alt: A.value.alt
535
+ }, null, 8, fe)], 8, de)) : n("", !0)
536
+ ]));
537
+ }
538
+ }), [["__scopeId", "data-v-e6edd9a7"]]);
539
+ //#endregion
540
+ export { Y as SeerChat, I as SeerChatPlugin, y as createSeerClient, M as linkAnchorLabel, P as toContentLines, N as toContentSegments, L as useSeerChat };
package/dist/plugin.d.ts CHANGED
@@ -2,6 +2,8 @@ import type { App, InjectionKey } from 'vue';
2
2
  import type { SeerClientOptions } from './client';
3
3
  export interface SeerChatPluginOptions extends SeerClientOptions {
4
4
  stream?: boolean;
5
+ voiceEnabled?: boolean;
6
+ maxRecordingSeconds?: number;
5
7
  }
6
8
  export declare const seerChatOptionsKey: InjectionKey<SeerChatPluginOptions>;
7
9
  export declare const SeerChatPlugin: {
@@ -18,6 +18,9 @@ export declare function useSeerChat(localOptions?: UseSeerChatOptions): {
18
18
  conversationId: import("vue").Ref<string | undefined, string | undefined>;
19
19
  citations: import("vue").Ref<string[], string[]>;
20
20
  handoffRequested: import("vue").Ref<boolean, boolean>;
21
+ transcriptionEnabled: boolean;
22
+ maxRecordingSeconds: number;
23
+ transcribe: (audio: Blob, filename: string, callbacks?: import("./client").SeerTranscriptionCallbacks) => Promise<import("./client").SeerTranscriptionResult>;
21
24
  send: (content: string) => Promise<void>;
22
25
  reset: () => void;
23
26
  abort: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seer-ai/seer-vue",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0-beta.7",
4
4
  "description": "Vue chat plugin for the seer OpenAI-compatible API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,11 +29,11 @@
29
29
  "vue": "^3.5.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@vitejs/plugin-vue": "^5.2.4",
32
+ "@vitejs/plugin-vue": "^6.0.7",
33
33
  "@vue/test-utils": "^2.4.10",
34
34
  "happy-dom": "^20.9.0",
35
35
  "typescript": "^6.0.3",
36
- "vite": "^6.4.2",
36
+ "vite": "^8.1.3",
37
37
  "vitest": "^4.1.7",
38
38
  "vue": "^3.5.35",
39
39
  "vue-tsc": "^3.3.2"