@seer-ai/seer-vue 0.1.0-beta.1 → 0.1.0-beta.10

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