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

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 ADDED
@@ -0,0 +1,58 @@
1
+ # @seer-ai/seer-vue
2
+
3
+ Vue 3 chat plugin for the seer OpenAI-compatible API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @seer-ai/seer-vue@beta
9
+ ```
10
+
11
+ ## Use the default chat component
12
+
13
+ ```ts
14
+ import { createApp } from 'vue'
15
+ import { SeerChatPlugin } from '@seer-ai/seer-vue'
16
+ import App from './App.vue'
17
+
18
+ createApp(App)
19
+ .use(SeerChatPlugin, {
20
+ endpoint: '/api/seer-chat',
21
+ model: 'seer',
22
+ stream: false,
23
+ })
24
+ .mount('#app')
25
+ ```
26
+
27
+ ```vue
28
+ <script setup lang="ts">
29
+ import { SeerChat } from '@seer-ai/seer-vue'
30
+ </script>
31
+
32
+ <template>
33
+ <SeerChat />
34
+ </template>
35
+ ```
36
+
37
+ ## Use custom UI
38
+
39
+ ```vue
40
+ <script setup lang="ts">
41
+ import { ref } from 'vue'
42
+ import { useSeerChat } from '@seer-ai/seer-vue'
43
+
44
+ const input = ref('')
45
+ const chat = useSeerChat()
46
+
47
+ async function submit() {
48
+ const content = input.value.trim()
49
+ if (!content) return
50
+ input.value = ''
51
+ await chat.send(content)
52
+ }
53
+ </script>
54
+ ```
55
+
56
+ ## Security
57
+
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.
@@ -0,0 +1,33 @@
1
+ export type SeerChatRole = 'system' | 'user' | 'assistant' | 'tool';
2
+ export interface SeerChatMessage {
3
+ role: SeerChatRole;
4
+ content: string;
5
+ name?: string;
6
+ }
7
+ export interface SeerClientOptions {
8
+ endpoint: string;
9
+ model?: string;
10
+ fetcher?: typeof fetch;
11
+ }
12
+ export interface SeerChatRequest {
13
+ messages: SeerChatMessage[];
14
+ model?: string;
15
+ stream?: boolean;
16
+ conversationId?: string;
17
+ metadata?: Record<string, unknown>;
18
+ }
19
+ export interface SeerChatCallbacks {
20
+ signal?: AbortSignal;
21
+ onDelta?: (delta: string) => void;
22
+ }
23
+ export interface SeerChatResult {
24
+ content: string;
25
+ conversationId?: string;
26
+ citations: string[];
27
+ handoffRequested: boolean;
28
+ finishReason?: string;
29
+ }
30
+ export interface SeerClient {
31
+ chat(request: SeerChatRequest, callbacks?: SeerChatCallbacks): Promise<SeerChatResult>;
32
+ }
33
+ export declare function createSeerClient(options: SeerClientOptions): SeerClient;
@@ -0,0 +1,17 @@
1
+ import type { SeerClientOptions } from '../client';
2
+ type __VLS_Props = {
3
+ endpoint?: string;
4
+ model?: string;
5
+ stream?: boolean;
6
+ fetcher?: SeerClientOptions['fetcher'];
7
+ title?: string;
8
+ placeholder?: string;
9
+ };
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
+ stream: boolean;
12
+ title: string;
13
+ placeholder: string;
14
+ model: string;
15
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
package/dist/index.css ADDED
@@ -0,0 +1 @@
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}
@@ -0,0 +1,7 @@
1
+ export { createSeerClient } from './client';
2
+ export { SeerChatPlugin } from './plugin';
3
+ export { useSeerChat } from './useSeerChat';
4
+ export { default as SeerChat } from './components/SeerChat.vue';
5
+ export type { SeerChatCallbacks, SeerChatMessage, SeerChatRequest, SeerChatResult, SeerChatRole, SeerClient, SeerClientOptions, } from './client';
6
+ export type { SeerChatPluginOptions } from './plugin';
7
+ export type { UseSeerChatOptions } from './useSeerChat';
package/dist/index.js ADDED
@@ -0,0 +1,269 @@
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
+ };
28
+ }
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
+ };
39
+ }
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
+ };
70
+ }
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
+ `);
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}`;
87
+ }
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
+ };
149
+ }
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
+ };
@@ -0,0 +1,9 @@
1
+ import type { App, InjectionKey } from 'vue';
2
+ import type { SeerClientOptions } from './client';
3
+ export interface SeerChatPluginOptions extends SeerClientOptions {
4
+ stream?: boolean;
5
+ }
6
+ export declare const seerChatOptionsKey: InjectionKey<SeerChatPluginOptions>;
7
+ export declare const SeerChatPlugin: {
8
+ install(app: App, options: SeerChatPluginOptions): void;
9
+ };
@@ -0,0 +1,24 @@
1
+ import { type SeerChatMessage } from './client';
2
+ import { type SeerChatPluginOptions } from './plugin';
3
+ export interface UseSeerChatOptions extends Partial<SeerChatPluginOptions> {
4
+ endpoint?: string;
5
+ }
6
+ export declare function useSeerChat(localOptions?: UseSeerChatOptions): {
7
+ messages: import("vue").Ref<{
8
+ role: import("./client").SeerChatRole;
9
+ content: string;
10
+ name?: string | undefined;
11
+ }[], SeerChatMessage[] | {
12
+ role: import("./client").SeerChatRole;
13
+ content: string;
14
+ name?: string | undefined;
15
+ }[]>;
16
+ loading: import("vue").Ref<boolean, boolean>;
17
+ error: import("vue").Ref<string | null, string | null>;
18
+ conversationId: import("vue").Ref<string | undefined, string | undefined>;
19
+ citations: import("vue").Ref<string[], string[]>;
20
+ handoffRequested: import("vue").Ref<boolean, boolean>;
21
+ send: (content: string) => Promise<void>;
22
+ reset: () => void;
23
+ abort: () => void;
24
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@seer-ai/seer-vue",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Vue chat plugin for the seer OpenAI-compatible API",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./style.css": "./dist/index.css"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "test": "vitest run",
24
+ "typecheck": "vue-tsc --noEmit",
25
+ "build": "vite build && vue-tsc -p tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist"
26
+ },
27
+ "peerDependencies": {
28
+ "vue": "^3.5.0"
29
+ },
30
+ "devDependencies": {
31
+ "@vitejs/plugin-vue": "^5.2.4",
32
+ "@vue/test-utils": "^2.4.10",
33
+ "happy-dom": "^20.9.0",
34
+ "typescript": "^6.0.3",
35
+ "vite": "^6.4.2",
36
+ "vitest": "^4.1.7",
37
+ "vue": "^3.5.35",
38
+ "vue-tsc": "^3.3.2"
39
+ }
40
+ }