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

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